# Read Room Data via WebSocket

Rooms group multiple Pulsoid users so their real-time heart rate data and membership changes can be consumed through a single WebSocket connection.

A client connects to a room's WebSocket endpoint and receives a stream of messages: heart rate updates from all room members, membership changes, and room configuration updates.

{% hint style="info" %}
Your application should implement reconnection logic. If the WebSocket connection drops, reconnect after a short delay.
{% endhint %}

***

#### Request

| Key            | Value                                                        |
| -------------- | ------------------------------------------------------------ |
| URL            | `wss://dev.pulsoid.net/api/v2/data/rooms/{roomId}/real_time` |
| Required scope | `data:room:read`                                             |

Replace `{roomId}` with the ID of the room you want to subscribe to. The connecting user must be a member of the room.

#### Authentication

Provide your OAuth2 Bearer token using one of the following methods:

| Method          | Example                                                                              |
| --------------- | ------------------------------------------------------------------------------------ |
| Query parameter | `wss://dev.pulsoid.net/api/v2/data/rooms/{roomId}/real_time?access_token=YOUR_TOKEN` |
| Header          | `Authorization: Bearer YOUR_TOKEN`                                                   |

#### Query Parameters

| Parameter      | Type     | Default      | Description                                                 |
| -------------- | -------- | ------------ | ----------------------------------------------------------- |
| `access_token` | `string` |              | OAuth2 access token (alternative to `Authorization` header) |
| `kinds`        | `string` | `heart_rate` | Comma-separated list of message kinds to subscribe to       |

#### Available Message Kinds

| Kind                  | Description                                    |
| --------------------- | ---------------------------------------------- |
| `heart_rate`          | Real-time heart rate updates from room members |
| `room_member_updated` | A member's profile or config was added/changed |
| `room_member_removed` | A member was removed from the room             |
| `room_updated`        | The room configuration was changed             |

To subscribe to multiple kinds, separate them with commas:

```
?kinds=heart_rate,room_member_updated,room_member_removed,room_updated
```

***

#### Response Messages

All messages are JSON objects with the following base structure:

| Field       | Type     | Description                                   |
| ----------- | -------- | --------------------------------------------- |
| `kind`      | `string` | The message kind (see table above)            |
| `timestamp` | `string` | ISO 8601 timestamp of when the event occurred |

Each message includes exactly one additional payload field matching its `kind`.

**`heart_rate`**

Contains the heart rate reading from a room member.

```json
{
  "kind": "heart_rate",
  "timestamp": "2026-02-21T12:00:00Z",
  "heart_rate": {
    "profile_id": "507f1f77bcf86cd799439011",
    "bpm": 85
  }
}
```

| Field                   | Type      | Description                          |
| ----------------------- | --------- | ------------------------------------ |
| `heart_rate.profile_id` | `string`  | Unique identifier of the room member |
| `heart_rate.bpm`        | `integer` | Heart rate in beats per minute       |

**`room_member_updated`**

Sent when a member is added to the room or their profile/config changes.

```json
{
  "kind": "room_member_updated",
  "timestamp": "2026-02-21T12:00:00Z",
  "room_member_updated": {
    "profile_id": "507f1f77bcf86cd799439011",
    "config": { "color": "#ff0000" }
  }
}
```

| Field                            | Type     | Description                                 |
| -------------------------------- | -------- | ------------------------------------------- |
| `room_member_updated.profile_id` | `string` | Unique identifier of the room member        |
| `room_member_updated.config`     | `object` | Arbitrary configuration data for the member |

**`room_member_removed`**

Sent when a member is removed from the room.

```json
{
  "kind": "room_member_removed",
  "timestamp": "2026-02-21T12:00:00Z",
  "room_member_removed": {
    "profile_id": "507f1f77bcf86cd799439011"
  }
}
```

| Field                            | Type     | Description                             |
| -------------------------------- | -------- | --------------------------------------- |
| `room_member_removed.profile_id` | `string` | Unique identifier of the removed member |

**`room_updated`**

Sent when the room configuration changes.

```json
{
  "kind": "room_updated",
  "timestamp": "2026-02-21T12:00:00Z",
  "room_updated": {
    "room_id": "room-abc-123",
    "config": { "theme": "dark", "layout": "grid" }
  }
}
```

| Field                  | Type     | Description                               |
| ---------------------- | -------- | ----------------------------------------- |
| `room_updated.room_id` | `string` | Identifier of the room                    |
| `room_updated.config`  | `object` | Arbitrary configuration data for the room |

***

#### Lazy Delivery of Initial State

When you first connect, you will **not** immediately receive room and member configuration messages. Instead, the server uses lazy delivery:

1. The room configuration (`room_updated`) is sent **once**, just before the first `heart_rate` message arrives.
2. Each member's configuration (`room_member_updated`) is sent **once**, when that member's first heart rate appears.

This means no messages arrive until heart rate data starts flowing, unless an explicit room or member update is triggered server-side.

{% hint style="warning" %}
If you need room and member configuration on connect, subscribe to `room_updated` and `room_member_updated` kinds. The configs will arrive automatically before the first heart rate data.
{% endhint %}

***

#### HTTP Status Codes

| Status | Description                                                                 |
| ------ | --------------------------------------------------------------------------- |
| `101`  | WebSocket upgrade successful                                                |
| `401`  | Missing or invalid token, or token does not have the `data:room:read` scope |
| `403`  | The authenticated user is not a member of the room                          |
| `500`  | Unexpected server error                                                     |

***

#### Connection Lifecycle

* The WebSocket connection will **automatically close** when the OAuth2 token expires.
* If a member is **removed from the room** while connected, their WebSocket connection is disconnected.
* The server sends only **text messages** (JSON). The client should ignore any incoming binary frames.

***

#### Example: JavaScript with `@pulsoid/socket`

For JavaScript and TypeScript applications, use the official [`@pulsoid/socket`](https://www.npmjs.com/package/@pulsoid/socket) library. It handles auto-reconnection, provides typed events, and has zero dependencies.

```bash
npm install @pulsoid/socket
```

```javascript
import PulsoidSocket from '@pulsoid/socket';

const room = PulsoidSocket.createRoom('YOUR_TOKEN', 'your-room-id');

room.on('heart-rate', (data) => {
  console.log(`${data.profileId}: ${data.bpm} BPM`);
});

room.on('room-member-updated', (data) => {
  console.log(`Member updated: ${data.profileId}`);
});

room.on('room-member-removed', (data) => {
  console.log(`Member removed: ${data.profileId}`);
});

room.on('room-updated', (data) => {
  console.log('Room config updated');
});

await room.connect();
```

See the [GitHub repository](https://github.com/pulsoid-oss/pulsoid-socket) for full documentation and configuration options.

#### Example: JavaScript with native WebSocket

```javascript
const roomId = "your-room-id";
const token = "your-access-token";
const kinds = "heart_rate,room_member_updated,room_member_removed,room_updated";

const url = `wss://dev.pulsoid.net/api/v2/data/rooms/${roomId}/real_time?access_token=${token}&kinds=${kinds}`;

const ws = new WebSocket(url);

ws.onopen = () => {
  console.log("Connected to room");
};

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);

  switch (message.kind) {
    case "heart_rate":
      console.log(`${message.heart_rate.profile_id}: ${message.heart_rate.bpm} BPM`);
      break;
    case "room_member_updated":
      console.log(`Member updated: ${message.room_member_updated.profile_id}`);
      break;
    case "room_member_removed":
      console.log(`Member removed: ${message.room_member_removed.profile_id}`);
      break;
    case "room_updated":
      console.log(`Room config updated`);
      break;
  }
};

ws.onclose = (event) => {
  console.log("Disconnected from room", event.code, event.reason);
  // Implement reconnection logic here
};

ws.onerror = (error) => {
  console.error("WebSocket error:", error);
};
```
