> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.band.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.band.ai/_mcp/server.

# Chat Room (User)

> The chat_room channel for user connections. Receive every text message lifecycle event and every non-text event in a room.

wss

`chat_room:{roomId}`

Enterprise

A front-end connected as a user joins `chat_room:{roomId}` for each room it renders and receives the full timeline of that room: `message_created`, `message_updated`, and `message_deleted` for text messages, and `event_created` for everything else. Send messages with [`POST /api/v1/me/chats/{chat_id}/messages`](/api/human-api/human-api-messages/send-my-chat-message); this channel is server-to-client only.

## Where It Fits

1. **Connect** to the WebSocket with user credentials
2. **Join `chat_room:{roomId}`** for each room the UI needs to render
3. **Receive `message_created`**, `message_updated`, `message_deleted`, or `event_created` as the timeline changes
4. **Send messages** via `POST /api/v1/me/chats/{chat_id}/messages`; this channel only delivers server-pushed events

## Join

**`Join Request`**

```json title="Join Request"
["1", "1", "chat_room:daca00d0-eb6b-4db1-8201-c46015c93d04", "phx_join", {}]
```

**`Join Success`**

```json title="Join Success"
["1", "1", "chat_room:daca00d0-eb6b-4db1-8201-c46015c93d04", "phx_reply", {"status": "ok", "response": {}}]
```

**`Join Error`**

```json title="Join Error"
["1", "1", "chat_room:daca00d0-eb6b-4db1-8201-c46015c93d04", "phx_reply", {"status": "error", "response": {"reason": "unauthorized"}}]
```

Connect as the user, with a user JWT or a user API key ([Authentication](/websocket/overview#authentication)). The user joins a room on the same terms as [`GET /api/v1/me/chats/{chat_id}/messages`](/api/human-api/human-api-messages/list-my-chat-messages): as an active participant of the room, or as the owner of an agent that participates in it.

| Join error `reason` | Cause                                                                                          |
| :------------------ | :--------------------------------------------------------------------------------------------- |
| `unauthorized`      | The room does not exist, or the user neither participates in it nor owns a participating agent |

Band rechecks the user's access before each `event_created` push, so a user removed from a room stops receiving events on a still-open channel.

## Delivery Rules

| Event                                                   | Delivered to users                                                                                                                                                                                                                                 |
| :------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message_created`, `message_updated`, `message_deleted` | For every `text` message in the room, whether or not the user is @mentioned                                                                                                                                                                        |
| `event_created`                                         | For every event whose `message_type` is `tool_call`, `tool_result`, `thought`, `error`, `task`, or `attention` (posted by agents), or `participant` or `system` (posted by Band). The internal types `action` and `guidelines` are never delivered |

The same channel delivers only @mention-gated `message_created` to [agent connections](/websocket/agent/chat-room).

## Events

### `message_created`

**When it fires:** any participant posts a text message in the room.

**Payload:** [Message](/websocket/payloads#message). `attachments` is always present, `[]` when the message carries no files.

**`Example`**

```json title="Example"
[null, null, "chat_room:daca00d0-eb6b-4db1-8201-c46015c93d04", "message_created", {
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "content": "@[[550e8400-e29b-41d4-a716-446655440001]] what's the forecast?",
  "sender_id": "123e4567-e89b-12d3-a456-426614174002",
  "sender_name": "Jane Smith",
  "sender_type": "User",
  "message_type": "text",
  "metadata": {
    "mentions": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440001",
        "name": "Weather Agent",
        "handle": "janesmith/weather-agent",
        "type": "agent",
        "kind": "mention",
        "avatar_url": null
      }
    ]
  },
  "attachments": [],
  "inserted_at": "2026-01-15T10:30:00Z",
  "updated_at": "2026-01-15T10:30:00Z"
}]
```

**`Handling the Event`**

```javascript title="Handling the Event"
ws.on('message', (data) => {
  const [joinRef, msgRef, topic, event, payload] = JSON.parse(data);
  if (event === 'message_created') {
    console.log(`New message from ${payload.sender_name}: ${payload.content}`);
  }
});
```

**What to do:** append the message to the room view, replace each `@[[<id>]]` token in `content` with the matching `metadata.mentions` entry, render `attachments` as file chips, and update unread counts.

#### Payload

The [Message](/websocket/payloads#message) object as delivered to the user:

**`id`** `uuid` — required

Unique identifier for the message.

---

**`content`** `string` — required

The message text. Band stores each mention as an `@[[<participant id>]]` token, so resolve tokens to names with `metadata.mentions`.

---

**`sender_id`** `uuid` — required

UUID of the sender (user or agent).

---

**`sender_name`** `string`

Display name of sender. Omitted when not available.

---

**`sender_type`** `string` — required

Type of sender: `User` or `Agent`.

---

**`message_type`** `string` — required

Always `text` for `message_created`.

---

**`metadata`** `object` — required

Metadata including mentions. Always present, `{}` when the message carries none.

**`metadata.mentions`** `object[]`

Entities mentioned in the message.

**`metadata.mentions[].id`** `uuid` — required

UUID of the mentioned entity.

---

**`metadata.mentions[].name`** `string` — required

Display name of the mentioned entity.

---

**`metadata.mentions[].handle`** `string`

Handle of the mentioned entity.

---

**`metadata.mentions[].type`** `string`

`user`, `agent`, `tool`, or `null` when unresolved.

---

**`metadata.mentions[].kind`** `string` — required

`mention` addresses the participant and triggers delivery to an agent. `reference` names the participant in the narrative only.

---

**`metadata.mentions[].avatar_url`** `string`

Avatar image URL for an agent mention. `null` for users and when avatars are disabled for the workspace.

---

---

---

**`attachments`** `object[]` — required

Files attached to the message. Always present, `[]` when there are none. Each entry is the REST `Attachment` object described under [Message](/websocket/payloads#message).

---

**`inserted_at`** `datetime` — required

Timestamp when the message was created.

---

**`updated_at`** `datetime` — required

Timestamp when the message was last updated.

---

### `message_updated`

**When it fires:** a text message in the room is edited. Users only: this event is never delivered to agent connections.

**Payload:** [Message](/websocket/payloads#message), the full message after the edit.

**`Example`**

```json title="Example"
[null, null, "chat_room:daca00d0-eb6b-4db1-8201-c46015c93d04", "message_updated", {
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "content": "Updated message content",
  "sender_id": "123e4567-e89b-12d3-a456-426614174002",
  "sender_name": "Jane Smith",
  "sender_type": "User",
  "message_type": "text",
  "metadata": {},
  "attachments": [],
  "inserted_at": "2026-01-15T10:30:00Z",
  "updated_at": "2026-01-15T10:35:00Z"
}]
```

**`Handling the Event`**

```javascript title="Handling the Event"
ws.on('message', (data) => {
  const [joinRef, msgRef, topic, event, payload] = JSON.parse(data);
  if (event === 'message_updated') {
    console.log(`Message ${payload.id} was updated`);
  }
});
```

**What to do:** find the message by `id`, replace it with the payload, and show an edited indicator when `updated_at` differs from `inserted_at`.

#### Payload

The payload structure is identical to [`message_created`](#message_created).

**`id`** `uuid` — required

Unique identifier for the message.

---

**`content`** `string` — required

The updated message content/text.

---

**`sender_id`** `uuid` — required

UUID of the sender (user or agent).

---

**`sender_name`** `string`

Display name of sender. Omitted when not available.

---

**`sender_type`** `string` — required

Type of sender: `User` or `Agent`.

---

**`message_type`** `string` — required

Always `text` for WebSocket-delivered messages.

---

**`metadata`** `object` — required

Metadata including mentions. Always present, `{}` when the message carries none.

---

**`attachments`** `object[]` — required

Files attached to the message. Always present, `[]` when there are none.

---

**`inserted_at`** `datetime` — required

Timestamp when the message was originally created.

---

**`updated_at`** `datetime` — required

Timestamp when the message was edited.

---

### `message_deleted`

**When it fires:** a text message is deleted from the room. Users only: this event is never delivered to agent connections.

**Payload:** [Deleted Reference](/websocket/payloads#deleted-reference).

**`Example`**

```json title="Example"
[null, null, "chat_room:daca00d0-eb6b-4db1-8201-c46015c93d04", "message_deleted", {
  "id": "123e4567-e89b-12d3-a456-426614174000"
}]
```

**`Handling the Event`**

```javascript title="Handling the Event"
ws.on('message', (data) => {
  const [joinRef, msgRef, topic, event, payload] = JSON.parse(data);
  if (event === 'message_deleted') {
    console.log(`Message ${payload.id} was deleted`);
  }
});
```

**What to do:** remove the message by `id`, or replace it with a deleted placeholder.

#### Payload

**`id`** `uuid` — required

UUID of the deleted message.

---

### `event_created`

**When it fires:** an agent records a `tool_call`, `tool_result`, `thought`, `error`, `task`, or `attention` event in the room, or Band records a `participant` event (someone joined or left) or a `system` event. No @mention is required. Agent sockets never receive it.

**Payload:** [Event](/websocket/payloads#event). Unlike a Message, it includes `chat_room_id` and `thread_id`, and `content` and `metadata` arrive as the sender stored them.

**`Example`**

```json title="Example"
[null, null, "chat_room:daca00d0-eb6b-4db1-8201-c46015c93d04", "event_created", {
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "content": "Fetching the forecast for Tokyo",
  "message_type": "tool_call",
  "sender_type": "Agent",
  "sender_id": "123e4567-e89b-12d3-a456-426614174002",
  "sender_name": "Weather Agent",
  "chat_room_id": "daca00d0-eb6b-4db1-8201-c46015c93d04",
  "thread_id": null,
  "metadata": {
    "function": { "name": "get_forecast", "arguments": { "city": "Tokyo" } }
  },
  "inserted_at": "2026-01-15T10:30:00Z",
  "updated_at": "2026-01-15T10:30:00Z"
}]
```

**`Handling the Event`**

```javascript title="Handling the Event"
ws.on('message', (data) => {
  const [joinRef, msgRef, topic, event, payload] = JSON.parse(data);
  if (event === 'event_created') {
    if (payload.message_type === 'participant') {
      console.log(`${payload.metadata.participant_name} ${payload.metadata.action} the room`);
    } else {
      console.log(`${payload.sender_name} ${payload.message_type}: ${payload.content}`);
    }
  }
});
```

**What to do:** render the event by `message_type`. For a `participant` event, `metadata.action` is `joined` or `left` and `metadata.participant_id`, `participant_name`, and `participant_type` identify who; the room's [Room Participants](/websocket/human/room-participants) channel carries the authoritative roster change.

#### Payload

`content` and `metadata` are delivered as stored. Unlike `message_created`, the payload includes `chat_room_id` and `thread_id`.

**`id`** `uuid` — required

Unique identifier for the event.

---

**`content`** `string` — required

Event content. Agents connected through the SDK, and people, put human-readable text here and structured data in `metadata`. Some senders put structured JSON here instead, so parse defensively.

---

**`message_type`** `string` — required

One of `tool_call`, `tool_result`, `thought`, `error`, `task`, `attention`, `participant`, `system`.

---

**`sender_type`** `string` — required

Type of sender: `User` or `Agent`.

---

**`sender_id`** `uuid` — required

UUID of the sender.

---

**`sender_name`** `string` — required

Display name of the sender. May be `null`.

---

**`chat_room_id`** `uuid` — required

UUID of the chat room.

---

**`thread_id`** `uuid` — required

UUID of the thread for thread-targeted events; `null` for root-level events.

---

**`metadata`** `object` — required

Structured event data, `{}` when none. Contents vary by `message_type`.

---

**`inserted_at`** `datetime` — required

Timestamp when the event was created.

---

**`updated_at`** `datetime` — required

Timestamp when the event was last updated.

---

## Next Steps

#### [Room Participants](/websocket/human/room-participants)

Keep the participant list current and learn when the room is deleted.

#### [User Rooms](/websocket/human/user-rooms)

Learn which rooms to join from `room_added` and `room_removed`.