> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Team Chat API: internal channels, threads, and huddles

> Internal team messaging for operators: channels, membership, threaded messages, huddles, and direct messages, kept separate from customer conversations.

# Team Chat API

Team Chat is internal, employee-to-employee messaging inside your Orbit workspace — channels, threaded replies, huddles (ad-hoc voice/video rooms), and direct messages between operators. It is entirely separate from customer-facing conversations: nothing here reaches a customer, and no outbound customer voice or SMS is touched by any endpoint on this page.

**Base path:** `/api/v1/team-chat`

**Authentication:** API key (`X-API-Key`) or session JWT. Posting or reading a channel requires channel membership; managing membership requires an `owner` or `admin` role on that channel. DM endpoints require the caller to be one of the two participants.

## Channels

| Method   | Path                                                      | Purpose                                        |
| -------- | --------------------------------------------------------- | ---------------------------------------------- |
| `GET`    | `/api/v1/team-chat/channels`                              | Channels the caller is a member of             |
| `POST`   | `/api/v1/team-chat/channels`                              | Create a channel (caller becomes owner)        |
| `DELETE` | `/api/v1/team-chat/channels/{channelId}`                  | Delete a channel and its messages (owner only) |
| `GET`    | `/api/v1/team-chat/channels/{channelId}/members`          | List members                                   |
| `POST`   | `/api/v1/team-chat/channels/{channelId}/members`          | Add a member (owner/admin only)                |
| `DELETE` | `/api/v1/team-chat/channels/{channelId}/members/{userId}` | Remove a member (owner/admin only)             |

## Channel messages

| Method   | Path                                                          | Purpose                                |
| -------- | ------------------------------------------------------------- | -------------------------------------- |
| `GET`    | `/api/v1/team-chat/channels/{channelId}/messages`             | Message history, newest-first          |
| `POST`   | `/api/v1/team-chat/channels/{channelId}/messages`             | Post a message                         |
| `PATCH`  | `/api/v1/team-chat/channels/{channelId}/messages/{id}`        | Edit your own message                  |
| `DELETE` | `/api/v1/team-chat/channels/{channelId}/messages/{id}`        | Soft-delete your own message           |
| `GET`    | `/api/v1/team-chat/channels/{channelId}/messages/{id}/thread` | Load a message thread (root + replies) |
| `POST`   | `/api/v1/team-chat/channels/{channelId}/messages/{id}/thread` | Reply in-thread to a message           |

## Search

| Method | Path                       | Purpose                                                     |
| ------ | -------------------------- | ----------------------------------------------------------- |
| `GET`  | `/api/v1/team-chat/search` | Full-text search messages across the channels you belong to |

`GET /api/v1/team-chat/search` ranks message bodies by relevance and paginates with an opaque `cursor`. It only ever searches channels you're a member of — membership is the authorization boundary, so a channel you were removed from drops out of results with no separate check.

| Query param   | Required | Description                                                                       |
| ------------- | -------- | --------------------------------------------------------------------------------- |
| `q`           | yes      | Search term, 1–200 characters. Matches are partial-word (`ship` finds `shipping`) |
| `channel_id`  | no       | Restrict results to a single channel                                              |
| `from` / `to` | no       | ISO timestamp bounds on `created_at`                                              |
| `cursor`      | no       | Opaque cursor from a previous page's response                                     |
| `limit`       | no       | Page size, 1–200 (default 25)                                                     |

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/team-chat/search?q=deploy&channel_id=tch_abc123" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": [
      {
        "id": "6d21f4a0-8b2c-45e1-9c77-0a1b2c3d4e5f",
        "channel_id": "tch_abc123",
        "channel_name": "on-call-platform",
        "sender_user_id": "usr_9k2hf7",
        "sender_name": "Riley Chen",
        "sender_avatar_url": "https://<your-cdn>/avatars/usr_9k2hf7/avatar.png",
        "body": "Deploy is out, watching error rates.",
        "mentions": [],
        "created_at": "2026-08-26T09:41:12.000Z",
        "edited_at": null,
        "rank": 0.8113
      }
    ],
    "meta": {
      "request_id": "req_m01ab2c3d4",
      "timestamp": "2026-08-26T09:41:15Z",
      "pagination": {
        "cursor": "eyJyIjowLjgxMTMsImMiOiIyMDI2LTA4LTI2VDA5OjQxOjEyLjAwMDAwMFoiLCJpIjoiNmQyMWY0YTAtOGIyYy00NWUxLTljNzctMGExYjJjM2Q0ZTVmIn0=",
        "has_more": true
      }
    }
  }
  ```
</ResponseExample>

<Note>
  The cursor is opaque — pass `meta.pagination.cursor` back verbatim as the `cursor` query param to fetch the next page, and do not parse or rebuild it. `has_more: false` (or a null cursor) means you have seen the full result set. Results are ordered by rank, then recency.
</Note>

## Huddles

A huddle is an ad-hoc, low-friction voice/video room scoped to a channel — anyone in the channel can start one or drop into the live one.

| Method | Path                                                  | Purpose                                          |
| ------ | ----------------------------------------------------- | ------------------------------------------------ |
| `GET`  | `/api/v1/team-chat/channels/{channelId}/huddle`       | Get the active huddle for a channel, if any      |
| `POST` | `/api/v1/team-chat/channels/{channelId}/huddle`       | Start a huddle, or join the one already running  |
| `POST` | `/api/v1/team-chat/channels/{channelId}/huddle/leave` | Leave the huddle (ends it when the room empties) |

## Direct messages

| Method   | Path                                        | Purpose                                             |
| -------- | ------------------------------------------- | --------------------------------------------------- |
| `GET`    | `/api/v1/team-chat/dms`                     | DM threads the caller participates in               |
| `POST`   | `/api/v1/team-chat/dms`                     | Open (get-or-create) a 1:1 thread with another user |
| `DELETE` | `/api/v1/team-chat/dms/{threadId}`          | Delete a thread and its messages (participant only) |
| `GET`    | `/api/v1/team-chat/dms/{threadId}/messages` | DM history                                          |
| `POST`   | `/api/v1/team-chat/dms/{threadId}/messages` | Send a DM                                           |
| `POST`   | `/api/v1/team-chat/dms/{threadId}/read`     | Mark inbound messages in a thread as read           |

## Members, threads, DMs, and huddles

Samples below state the response envelope for one call in each declared group. `data` is the payload shown under each example; every response also carries `meta.request_id` (echo it back to support — it is the fastest cross-reference) and `meta.timestamp`.

### Add a member

`POST /api/v1/team-chat/channels/{channelId}/members` with `{ "user_id": "<workspace user id>" }` adds one member. Only the channel's owner or an admin can add or remove members; a caller without that role gets `403 NOT_A_MEMBER`. Response `201`:

```json 201 theme={null}
{
  "data": {
    "channel_id": "tch_abc123",
    "user_id": "usr_9k2hf7",
    "role": "member",
    "joined_at": "2026-08-26T09:42:31.000Z"
  },
  "meta": { "request_id": "req_m01ab2c3d7", "timestamp": "2026-08-26T09:42:32Z" }
}
```

Re-adding an existing member is idempotent and returns the same shape with the refreshed role and joined timestamp.

### Reply in a thread

`POST /api/v1/team-chat/channels/{channelId}/messages/{messageId}/thread` posts into the thread rooted at `{messageId}`. Threads are one level deep: if `{messageId}` is itself a reply, the new reply still points at that reply's root, so `parent_message_id` always names the root message. Response `201`:

```json 201 theme={null}
{
  "data": {
    "id": "7e33b921-1c4d-46f2-88aa-1b2c3d4e5f6a",
    "channel_id": "tch_abc123",
    "parent_message_id": "6d21f4a0-8b2c-45e1-9c77-0a1b2c3d4e5f",
    "sender_user_id": "usr_9k2hf7",
    "body": "Error rate steady. Root cause was a stale pod.",
    "mentions": [],
    "attachments": [],
    "created_at": "2026-08-26T09:44:07.000Z",
    "edited_at": null
  },
  "meta": { "request_id": "req_m01ab2c3d8", "timestamp": "2026-08-26T09:44:08Z" }
}
```

`GET` on the same path returns the thread in reading order as `{ "root": <message>, "replies": [<message>...] }`; a soft-deleted root renders `root: null` with the surviving replies.

### List DM threads

`GET /api/v1/team-chat/dms` returns the threads you participate in, most-recent-message first. Response `200`:

```json 200 theme={null}
{
  "data": [
    {
      "id": "dm_8f4b2a",
      "counterparty": "usr_3m9qz1",
      "counterparty_name": "Sam Ortiz",
      "counterparty_avatar_url": "https://<your-cdn>/avatars/usr_3m9qz1/avatar.png",
      "created_at": "2026-08-20T15:02:44.000Z",
      "last_body": "Can you pick up the changelog review?",
      "last_message_at": "2026-08-26T08:56:10.000Z",
      "unread_count": 2
    }
  ],
  "meta": { "request_id": "req_m01ab2c3d9", "timestamp": "2026-08-26T09:45:01Z" }
}
```

Each row is a thread id plus the counterparty's hydrated identity (name and avatar fall back to `null` — render the id tail then), their last inbound message preview (`last_body`, capped at 140 characters), and your capped `unread_count`. DM endpoints reject callers who are not one of the two participants.

### Start or join a huddle

`POST /api/v1/team-chat/channels/{channelId}/huddle` starts the huddle (or rejoins the one already running) and returns a join hint to pass to your WebRTC client — this is an internal operator voice/video plane; nothing on this page exercises outbound customer voice or SMS. Response `201` (first start) or `200` (joining a live huddle):

```json 201 theme={null}
{
  "data": {
    "started": true,
    "huddle": {
      "room_name": "tnt-1a2b3c4d:chan-tch_abc123",
      "channel_id": "tch_abc123",
      "started_by": "usr_4t7qn2",
      "started_at": "2026-08-26T09:46:00.000Z",
      "media": "audio"
    },
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.signature",
    "room_name": "tnt-1a2b3c4d:chan-tch_abc123",
    "media_url": "wss://media.orbit.devotel.io"
  },
  "meta": { "request_id": "req_m01ab2d0f0", "timestamp": "2026-08-26T09:46:01Z" }
}
```

`started: true` means you opened the room; `started: false` means you joined it. Present `token` around `media_url` as the client join hint — the server pre-creates the room with an empty-timeout so an abandoned huddle reclaims itself, and `POST /huddle/leave` ends it when the room empties.

## Example — create a channel and post a message

```bash theme={null}
# Create a channel
curl -X POST https://api.orbit.devotel.io/api/v1/team-chat/channels \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "on-call-platform", "description": "Platform team on-call chatter" }'

# Post a message
curl -X POST https://api.orbit.devotel.io/api/v1/team-chat/channels/tch_abc123/messages \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Deploy is out, watching error rates." }'
```

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "tch_abc123",
      "name": "on-call-platform",
      "description": "Platform team on-call chatter",
      "created_by_user_id": "usr_4t7qn2",
      "created_at": "2026-08-26T09:40:01.000Z",
      "updated_at": "2026-08-26T09:40:01.000Z",
      "my_role": "owner"
    },
    "meta": { "request_id": "req_m01ab2c3d5", "timestamp": "2026-08-26T09:40:02Z" }
  }
  ```
</ResponseExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "6d21f4a0-8b2c-45e1-9c77-0a1b2c3d4e5f",
      "channel_id": "tch_abc123",
      "sender_user_id": "usr_4t7qn2",
      "body": "Deploy is out, watching error rates.",
      "mentions": [],
      "attachments": [],
      "created_at": "2026-08-26T09:41:12.000Z",
      "edited_at": null
    },
    "meta": { "request_id": "req_m01ab2c3d6", "timestamp": "2026-08-26T09:41:15Z" }
  }
  ```
</ResponseExample>

Channel objects carry `id`, `name`, `description`, `created_by_user_id`, `created_at`, `updated_at`, plus `my_role` — your role on the channel, `owner` for its creator. Message objects carry `id`, `channel_id`, `sender_user_id`, `body`, `mentions`, `attachments`, `created_at`, and `edited_at`; a top-level message has `parent_message_id: null` (replies point at their thread root). The workspace user id you see as `created_by_user_id` / `sender_user_id` qualifies as an `owner_id` / `author_id` wherever a client types that field — only members yield ids on this surface (403 `NOT_A_MEMBER` otherwise).

## Walked mini-recipe — create, post, edit, and search

The four-step miniature below recreates a `create channel → post → edit own message → search it` flow end to end; drop the drafts into the Task index in [API recipes](/guides/api-recipes). Shape it as copyable steps and keep every request id so a page hop across the four calls stays traceable.

**Create** the channel — capture `data.id` (`tch_…`) for the two follows:

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/team-chat/channels \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "incident-alpha", "description": "Spillover incident channel" }'
```

**Post** the original message — capture `data.id` (the message id):

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/team-chat/channels/tch_abc123/messages \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Root-cause meeting in 10" }'
```

**Edit own message** — only the author can `PATCH`; the sender-scoped WHERE clause is the authorization boundary, so another member gets `404 NOT_FOUND`:

```bash cURL theme={null}
curl -X PATCH https://api.orbit.devotel.io/api/v1/team-chat/channels/tch_abc123/messages/6d21f4a0-8b2c-45e1-9c77-0a1b2c3d4e5f \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Root-cause meeting in 15" }'
```

**Search it** — your membership on that channel is the only gate:

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/team-chat/search?q=root-cause&channel_id=tch_abc123" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Each hit sits directly in `data` (a flat array, `data.results` if your client normalizes it) in rank-descending order, and `meta.pagination.cursor` carries forward the next-page token. Reuse the cursor verbatim — parsing or rebuilding it voids the page; an unparsed/`null` cursor always starts at rank 1.

## See also

* [On-Call API](/api-reference/oncall) — a common pairing: page a rotation, then coordinate the incident in a team-chat channel
* [Inbox API](/api-reference/inbox) — the customer-facing conversation surface this is intentionally separate from
