> ## 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.

# TeamChat orientation: worked channel + message round-trip

> Create an internal channel, post a message with @-mentions, read the history back with sender identity hydrated, and react to a message — with the full response bodies to copy.

## The channel and message resources

TeamChat is internal, workspace-to-workspace messaging: a channel is one
thread of conversation scoped to your organization, and a message is one post
inside it. A channel carries its `id`, `name`, and (optional) `description`;
each message carries a `body` plus two fields your own integrations write —
`mentions` (resolved user ids) and `attachments`.

Membership is the authorization boundary: you must be a member of a channel
to read its history or post to it, and only `owner`/`admin` roles manage
members. When you create a channel, you join it automatically as `owner`.

Every response on this page follows the same `{ data, meta }` envelope — a
`data` payload plus a `meta` block carrying `request_id` and `timestamp` — so
the examples below hold for every endpoint here. Errors follow Devotel Orbit's
`{ error, meta }` envelope, shown once under **Error envelope**.

## Worked sequence: create, post, read back

The run-through every integration does first: create the channel, post a
message that mentions a teammate, then read the history back with sender
identity hydrated.

### 1. Create the channel

<Note>
  `POST /api/v1/team-chat/channels`
</Note>

Only `name` is required (1–120 characters). `description` is optional (up to
500 characters). The caller is inserted as the channel's `owner` member.

**Request**

```json theme={null}
{
  "name": "launch-war-room",
  "description": "Coordination channel for the fall product launch"
}
```

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "9f2c1b7a-4d3e-4b8f-9a1c-2e7d5f8b0a41",
      "name": "launch-war-room",
      "description": "Coordination channel for the fall product launch",
      "created_by_user_id": "user_2abc3def4ghi5jkl6mnopqr",
      "created_at": "2026-08-28T09:15:00.000Z",
      "updated_at": "2026-08-28T09:15:00.000Z",
      "my_role": "owner"
    },
    "meta": {
      "request_id": "req_create_channel",
      "timestamp": "2026-08-28T09:15:00.000Z"
    }
  }
  ```
</ResponseExample>

The response carries `my_role: "owner"` as a convenience — the channels list
endpoint returns the same field on every row you are a member of.

### 2. Post a message with @-mentions

<Note>
  `POST /api/v1/team-chat/channels/{channelId}/messages`
</Note>

A message needs a `body` or at least one `attachment` — an empty post is
rejected with `422`. `body` accepts plain text or Slack-style markdown
(`**bold**`, `_italic_`, `~strike~`, back-tick code blocks, and lists) and is
capped at 8000 characters.

`mentions` is an array of resolved user ids — write `@name` in the body text
for the reader and pass the same person's user id in `mentions` so the
notification fan-out knows whom to ping without re-parsing your text.

**Request**

```json theme={null}
{
  "body": "Supplier contract draft is in the shared drive. @Ada Lovelace please review the liability clause before Friday.",
  "mentions": ["user_2xyz7wvu8tsr9qpo1nmlkjih"]
}
```

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "3a4b5c6d-7e8f-4a2f-8b3d-1c4e5f6a7b8c",
      "channel_id": "9f2c1b7a-4d3e-4b8f-9a1c-2e7d5f8b0a41",
      "sender_user_id": "user_2abc3def4ghi5jkl6mnopqr",
      "body": "Supplier contract draft is in the shared drive. @Ada Lovelace please review the liability clause before Friday.",
      "mentions": ["user_2xyz7wvu8tsr9qpo1nmlkjih"],
      "attachments": [],
      "created_at": "2026-08-28T09:16:30.000Z",
      "edited_at": null
    },
    "meta": {
      "request_id": "req_post_message",
      "timestamp": "2026-08-28T09:16:30.000Z"
    }
  }
  ```
</ResponseExample>

`edited_at` is `null` until somebody edits the message. Posting a message also
pushes a realtime refresh hint to every open dashboard and creates a
persistent notification for each other channel member, so an empty client poll
is never required to learn about a new post.

### 3. Read the channel history back

<Note>
  `GET /api/v1/team-chat/channels/{channelId}/messages`
</Note>

Returns thread roots (top-level posts) newest-first, bounded by the `limit`
query parameter — default `50`, maximum `200`. Replies live inside their
parent's thread so a busy thread never floods the channel feed; each root
carries `reply_count` and `last_reply_at` when it has replies.

The `GET` hydrates each row with the sender's human-readable identity from
your user directory — `sender_name` and `sender_avatar_url` — so a UI renders
a person, not the internal `sender_user_id`. When the sender row was deleted
(or was a system sender) both fields come back `null` and the id stays.

```bash theme={null}
curl -X GET "https://api.orbit.devotel.io/api/v1/team-chat/channels/9f2c1b7a-4d3e-4b8f-9a1c-2e7d5f8b0a41/messages?limit=50" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": [
      {
        "id": "3a4b5c6d-7e8f-4a2f-8b3d-1c4e5f6a7b8c",
        "channel_id": "9f2c1b7a-4d3e-4b8f-9a1c-2e7d5f8b0a41",
        "sender_user_id": "user_2abc3def4ghi5jkl6mnopqr",
        "body": "Supplier contract draft is in the shared drive. @Ada Lovelace please review the liability clause before Friday.",
        "mentions": ["user_2xyz7wvu8tsr9qpo1nmlkjih"],
        "attachments": [],
        "created_at": "2026-08-28T09:16:30.000Z",
        "edited_at": null,
        "reply_count": 0,
        "last_reply_at": null,
        "sender_name": "Grace Hopper",
        "sender_avatar_url": "https://avatar.example.com/grace.png"
      }
    ],
    "meta": {
      "request_id": "req_list_messages",
      "timestamp": "2026-08-28T09:17:00.000Z"
    }
  }
  ```
</ResponseExample>

### 4. React to a message

<Note>
  `POST /api/v1/team-chat/channels/{channelId}/messages/{messageId}/reactions`
</Note>

One reaction per user per emoji — repeating the POST with the same emoji is a
no-op. The reaction feeds the per-message summary your UI polls with
`GET …/messages/reactions?message_ids=…`.

**Request**

```json theme={null}
{
  "emoji": "🎉"
}
```

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "emoji": "🎉",
      "message_id": "3a4b5c6d-7e8f-4a2f-8b3d-1c4e5f6a7b8c"
    },
    "meta": {
      "request_id": "req_add_reaction",
      "timestamp": "2026-08-28T09:17:20.000Z"
    }
  }
  ```
</ResponseExample>

## Error envelope

Schema failures return `422` with a `VALIDATION_ERROR` code and the offending
fields listed under `details`. The classic case is an empty message — no
`body`, no `attachments`:

```json 422 theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "status": 422,
    "message": "Invalid message body"
  },
  "meta": {
    "request_id": "req_...",
    "timestamp": "2026-08-28T09:16:30.000Z"
  }
}
```

Reading or posting to a channel you do not belong to returns `403`
`NOT_A_MEMBER` — handle it before you code a retry loop. On the creation
path a 500 means the insert itself failed; channel names and descriptions are
server-scrubbed only for length, so a narrower client-side validation never
shadows a server accept.
