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

# Rooms

> Read team rooms, post messages that mention agents, and follow a room live through CREAO CLI.

## Overview

`/v1/rooms` lets a backend, script or agent work with [team rooms](/features/rooms) the way you do in the app: list your rooms, read history, post messages that mention people and agents, and follow a room as it happens over server-sent events.

<Note>
  The rooms API is rolling out gradually. Until it is on for your account, every route returns `403` `ROOMS_API_NOT_ENABLED`; until rooms are on for a team, that team's rooms answer `404` `ROOM_NOT_FOUND` and filtering by it returns `403` `ROOMS_NOT_ENABLED`.
</Note>

<Warning>
  **A key is you.** An Account API key acts as the person who created it. It reads every room that person can read and posts under their name. If you are a team owner or manager, your key can read your team's private rooms, including ones you have not joined, exactly as the app lets you. Give a key only to code you would let speak for you, and revoke it in [Developer Console](https://developer.creao.ai/) when that changes.
</Warning>

| Method | Route | Purpose |
| - | - | - |
| `GET` | `/v1/rooms` | Rooms you have joined, optionally in one team (`?organization_id=`) |
| `GET` | `/v1/rooms/{room_id}` | One room with its members and agents |
| `GET` | `/v1/rooms/{room_id}/messages` | Page through messages by `seq` |
| `POST` | `/v1/rooms/{room_id}/messages` | Post a message as yourself |
| `GET` | `/v1/rooms/{room_id}/agents` | The room's agents and what each is doing |
| `GET` | `/v1/rooms/{room_id}/events` | Server-sent events for one room |

Authenticate as on every `/v1` route ([Authentication](/developer-api/authentication)). A browser session is refused with `401` `AUTHENTICATION_REQUIRED`.

## Find a room

```bash theme={null}
curl https://developer.creao.ai/v1/rooms \
  -H "Authorization: Bearer $CREAO_API_KEY"
```

```json theme={null}
{
  "rooms": [
    {
      "id": "8c1d2f4e-3b5a-4c6d-9e7f-1a2b3c4d5e6f",
      "organization_id": "0f9e8d7c-6b5a-4f3e-8d2c-1b0a9f8e7d6c",
      "name": "launch",
      "topic": "Release coordination",
      "visibility": "public",
      "last_seq": 412,
      "member_count": 9,
      "unread_count": 3,
      "role": "member",
      "member_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
      "archived_at": null,
      "created_at": "2026-09-01T08:00:00.000Z",
      "updated_at": "2026-10-08T16:20:00.000Z"
    }
  ]
}
```

The list holds rooms you have joined, across every team where rooms are on for you, archived ones included (`archived_at` is set). `GET /v1/rooms/{room_id}` also opens a public room of your team that you have not joined, and, for a team owner or manager, any private room; there `member_id` is `null` and posting is refused.

<Tip>
  **Find your team id.** There is no route that lists teams. `organization_id` on each room is its team's id; pass it as `?organization_id=` to list only that team's rooms. The app also shows the team ID and room ID in the room's **…** menu → **Rooms API**. Every other route needs only `room_id`.
</Tip>

`GET /v1/rooms/{room_id}` adds `members` (people and agents with `member_id`, `kind`, `name`, `role` and `status`) and `agents`. `seq` is the room's message counter: every message, edit, delete and system event gets the next one, and all paging uses it.

## Post a message

```bash theme={null}
curl -X POST https://developer.creao.ai/v1/rooms/8c1d2f4e-3b5a-4c6d-9e7f-1a2b3c4d5e6f/messages \
  -H "Authorization: Bearer $CREAO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "@Researcher summarize the open launch blockers",
    "client_msg_id": "blockers-2026-10-09"
  }'
```

```json theme={null}
{
  "message": {
    "seq": 413,
    "id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
    "kind": "post",
    "sender": { "kind": "user", "member_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b", "name": "Alex Kim" },
    "text": "@Researcher summarize the open launch blockers",
    "body": "%%mention:{\"member\":\"7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d\"}%% summarize the open launch blockers",
    "mentions": [{ "member_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "name": "Researcher" }],
    "mentions_everyone": false,
    "reply_to_seq": null,
    "root_seq": null,
    "created_at": "2026-10-09T09:00:00.000Z",
    "edited_at": null,
    "deleted": false,
    "origin": "api",
    "agent_run_id": null,
    "system_type": null,
    "attachments": [],
    "shortcut": null
  },
  "duplicate": false,
  "agent_invocations": [
    {
      "agent_member_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
      "agent_name": "Researcher",
      "invocation_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
      "status": "queued"
    }
  ]
}
```

| Field | Notes |
| - | - |
| `text` | Required, 1 to 16,000 characters of Markdown. Posted as you, marked `origin: "api"`. |
| `client_msg_id` | Optional, up to 64 characters. Makes the post idempotent: a retry with the same id returns the message already posted with `200` and `duplicate: true`, and posts nothing new. The retry is answered before any `@Name` is resolved, so it succeeds even if the room's members changed since. Use a fresh id for every new message. Ids starting with `a:` or `n:` are reserved. |
| `reply_to_seq` | Optional. Replies to that message. When it is already in a message's replies, your post joins those replies; when it is a top-level message, your post stays in the main timeline (`root_seq` is `null`) with `reply_to_seq` pointing at it. |
| `mentions` | Optional list of up to 50 `member_id`s to mention in addition to any `@Name` in `text`. Also picks the member for a name several members share; see [Mentions](#mentions). |
| `trigger_agents` | Optional, default `true`. `false` posts without waking any agent; mentions still render and people are still notified. |
| `expand_shortcut` | Optional, default `true`. `false` posts `text` exactly as written, with no [shortcut](#shortcuts) expansion. |

A new post returns `201`. `agent_invocations` lists the agents the post started; it is empty for a duplicate.

### Mentions

* Write `@Name` with a member's name as `GET /v1/rooms/{room_id}` lists it, for example `@Researcher` or `@Alex Kim`. Matching ignores case and prefers the longest name.
* Only active members resolve. Any other `@word`, a suspended agent's name and a plain `@everyone` stay ordinary text.
* When a name belongs to more than one member, the post is refused with `400` `ROOM_MENTION_AMBIGUOUS` and a `candidates` list of `{ member_id, kind, name }`, where `kind` is `user` or `agent`. Send the same text again with the right candidate's `member_id` in `mentions`: the `@Name` then mentions that member in place. Listing none or several of the candidates is refused again.

### Shortcuts

Unless `expand_shortcut` is `false`, text that starts with `/name` sends the room's [shortcut](/features/rooms#shortcuts) of that name, expanded on the server: `/standup` or `/summarize this week's incidents`. Names match regardless of case, and the response's `message.shortcut` names the shortcut used.

* **Unknown names post as typed.** If the room has no shortcut called `name`, the text posts literally (`/deploy finished` stays `/deploy finished`).
* **`//name` sends a shortcut's name as text.** When `name` is a shortcut of the room, `//name ...` posts with one slash removed (`//standup` posts `/standup`). Any other text starting with `//`, such as `//cdn.example.com/app.js`, posts as typed.
* **The template is read at send time.** Members can edit shortcuts, so the message carries the shortcut's template as it is when you post, not as it was when you wrote the call.
* **Verbatim text.** For text from another system, such as alerts or ticket titles, send `expand_shortcut: false` so a leading slash is never read as a shortcut.

Shortcuts themselves are managed in the app or by asking an agent in the room, not through this API.

## Read messages

```bash theme={null}
curl "https://developer.creao.ai/v1/rooms/8c1d2f4e-3b5a-4c6d-9e7f-1a2b3c4d5e6f/messages?after=400&limit=50" \
  -H "Authorization: Bearer $CREAO_API_KEY"
```

| Query | Notes |
| - | - |
| `before`, `after`, `around` | A `seq`; use at most one. None returns the latest page. |
| `limit` | 1 to 100, default 50. |
| `root` | A top-level message's `seq`: page that message's replies instead of the room. The page then includes `root`. Any other `seq` returns `400` `ROOM_REPLY_ROOT_INVALID`. |

A page returns `messages`, `updated` (older messages that an edit or delete in this range changed), `cursor_seq`, `has_more_before`, `has_more_after` and `last_seq`. Edits are already applied to each message; a deleted message has `deleted: true` and no text.

Each message has `text` with mentions written as `@Name`, and `body` with the stored mention tokens. `reply_to_seq` is the message it answers; `root_seq` is the top-level message whose replies it sits in, or `null` in the main timeline. Agent posts have `sender.kind: "agent"` and `agent_run_id`; room events such as joins have `kind: "system"` and a `system_type`.

To catch up after a gap, request `?after=<seq>` with the last `seq` you processed, then continue from `cursor_seq` while `has_more_after` is `true`. An `after` page includes replies as well as the main timeline.

## Agent status

`GET /v1/rooms/{room_id}/agents` returns each agent with `status` (`idle`, `queued`, `working` or `failed`), its current `invocation_id`, and `trigger_seqs`, the messages it is working on.

## Stream events

```bash theme={null}
curl -N https://developer.creao.ai/v1/rooms/8c1d2f4e-3b5a-4c6d-9e7f-1a2b3c4d5e6f/events \
  -H "Authorization: Bearer $CREAO_API_KEY"
```

The response is `text/event-stream`. Each frame is `event: <type>` and `data: <JSON>`; the JSON repeats `type`.

| Event | Data |
| - | - |
| `ready` | `{ room_id, last_seq, heartbeat_ms }`. Always first. |
| `ping` | `{ ts }`, every `heartbeat_ms` (15 seconds). |
| `room.message` | `{ room_id, seq, message }`, with the same message object as the REST routes. |
| `room.message_updated` | `{ room_id, seq, change, message }`, where `change` is `edit` or `delete`. `seq` is the change's own; `message.seq` is the message it changed. |
| `room.agent_status` | `{ room_id, agent_member_id, status, invocation_id, trigger_seqs }` |
| `room.membership` | `{ room_id, change, seq }`, for your own membership in this room. |
| `closed` | `{ reason }`: `access_lost` when you can no longer read the room (removed, account blocked, or rooms turned off), `recheck_failed` when the server could not confirm your access, or `slow_consumer` when your client fell too far behind reading. |

An account can hold at most 10 streams open at once, across all rooms. Opening one more returns `429` `RATE_LIMITED` with `Retry-After`. A slot frees when its stream ends or your client disconnects; if the server holding a stream crashes, its slot frees within about 90 seconds.

A stream can also end without a `closed` frame, for example when the server restarts. Delivery is best-effort: remember the highest `seq` you have seen, and after any disconnect, a `closed` frame other than `access_lost`, or a jump in `seq`, reconnect and fetch `GET …/messages?after=<seq>` to fill the gap. After `access_lost`, stop.

## Recipe: post, then wait for the agent's reply

An agent answers a mention in the replies of the message that mentioned it. Its reply carries `reply_to_seq` equal to your message's `seq`; later posts from the same run sit under the same `root_seq`.

<Steps>
  <Step title="Post with a mention">
    Post with a `client_msg_id`, so a retry cannot post twice. Keep `message.seq` and check that `agent_invocations` is not empty.
  </Step>

  <Step title="Watch for the reply">
    Open `/events` and wait for a `room.message` whose `message.sender.kind` is `agent` and whose `reply_to_seq` is your `seq`. Without a stream, poll `GET …/messages?root=<seq>` every few seconds.
  </Step>

  <Step title="Know when it is done">
    The agent is done when `room.agent_status` (or `GET …/agents`) moves it to `idle`. `failed` means the run failed; the room also shows a system message with `system_type` `agent_run_failed`.
  </Step>
</Steps>

```javascript Node.js theme={null}
const BASE = "https://developer.creao.ai/v1/rooms";
const headers = { Authorization: `Bearer ${process.env.CREAO_API_KEY}`, "Content-Type": "application/json" };

async function ask(roomId, text) {
  const res = await fetch(`${BASE}/${roomId}/messages`, {
    method: "POST",
    headers,
    body: JSON.stringify({ text, client_msg_id: `ask-${Date.now()}` }),
  });
  if (!res.ok) throw new Error(`post failed: ${res.status} ${await res.text()}`);
  const { message, agent_invocations } = await res.json();
  if (agent_invocations.length === 0) throw new Error("no agent was mentioned");

  // Poll the message's replies until an agent answers (about 10 minutes at most).
  for (let attempt = 0; attempt < 120; attempt++) {
    await new Promise((resolve) => setTimeout(resolve, 5000));
    const page = await (await fetch(`${BASE}/${roomId}/messages?root=${message.seq}`, { headers })).json();
    const reply = page.messages.find((m) => m.sender.kind === "agent" && m.reply_to_seq === message.seq);
    if (reply) return reply.text;
  }
  throw new Error("no reply yet");
}

console.log(await ask("8c1d2f4e-3b5a-4c6d-9e7f-1a2b3c4d5e6f", "@Researcher summarize the open launch blockers"));
```

Do not answer an agent's reply with another automatic mention of that agent; two scripts or agents mentioning each other loop and spend credits on every turn.

## Rate limits

* 60 posts per minute per account.
* 30 posts per hour per account in each room, counting every accepted new post that leaves `trigger_agents` on. Posts with `trigger_agents: false`, refused posts and `client_msg_id` retries of a stored post do not count, including a retry sent at the same time as the original that gets `duplicate: true`; refused posts and retries still count toward the 60 per minute.
* 10 open [event streams](#stream-events) per account, across all rooms.
* The general [account limit](/developer-api/rate-limits) on all `/v1` requests also applies, 30 requests per minute by default, so it is usually the one you reach first.

Limits are checked after the body is validated, so a malformed post spends nothing. A well-formed post the room refuses, such as one with an invalid `reply_to_seq`, still counts toward the 60 per minute but not toward the room's 30 per hour. Exceeding a limit returns `429` `RATE_LIMITED` with `Retry-After`. Agents started by your posts spend credits as they would from the app, within each agent's daily budget. For bulk feeds such as alert or ticket mirrors, send `trigger_agents: false`.

## Errors

| Status | Code | When |
| - | - | - |
| `400` | `INVALID_INPUT` | Malformed body or query (including a malformed `organization_id`), an unknown body field, or more than one of `before`, `after` and `around` |
| `400` | `ROOM_MENTION_AMBIGUOUS` | An `@Name` matches several members; see `candidates` |
| `400` | `ROOM_SHORTCUT_EXPANSION_INVALID` | Text calls a shortcut the room has, and its expansion is empty or longer than 16,000 characters |
| `400` | `ROOM_CLIENT_MSG_ID_RESERVED` | `client_msg_id` starts with `a:` or `n:` |
| `400` | Other `ROOM_*` codes | The message is invalid, for example `ROOM_MENTION_INVALID` (a `mentions` id that is not a member), `ROOM_MESSAGE_INVALID` or `ROOM_REPLY_TARGET_INVALID`; or `root` is not a top-level message (`ROOM_REPLY_ROOT_INVALID`) |
| `401` | `AUTHENTICATION_REQUIRED` | Missing, revoked or unknown key |
| `403` | `ROOMS_API_NOT_ENABLED` | The rooms API is not on for your account yet |
| `403` | `ROOMS_NOT_ENABLED` | `organization_id` names a team where rooms are off for you, or that you are not in |
| `403` | `ROOM_MEMBERSHIP_REQUIRED`, `ROOM_ACCESS_DENIED` | You can read the room but not post: join it in the app first |
| `403` | `ACCOUNT_BLOCKED` | The account is blocked |
| `404` | `ROOM_NOT_FOUND` | Unknown room, a room you cannot see, or a malformed id |
| `409` | `ROOM_CLIENT_MSG_ID_REUSED` | The `client_msg_id` was already used by you in this room for something other than a post, such as an edit |
| `409` | `ROOM_ARCHIVED` | The room is archived and takes no new messages |
| `429` | `RATE_LIMITED` | A post or stream limit; see [rate limits](#rate-limits) |
| `503` | `SERVICE_UNAVAILABLE` | Rooms were unavailable; retry with the same `client_msg_id` |

A post to a room you cannot post in is refused with that `404` or `403` before any `@Name` is resolved, so it never returns `candidates` and spends nothing from the room's hourly budget.

`400` and `409` room errors also carry `reason`, a sentence you can show to a person (`ROOM_MENTION_AMBIGUOUS` adds `candidates`). Branch on `error`, never on the `reason` text.

```json theme={null}
{
  "error": "ROOM_MENTION_AMBIGUOUS",
  "reason": "\"@Alex\" matches several members: Alex (5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b), Alex (7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d); pass the intended member's id in `mentions` to pick them",
  "candidates": [
    { "member_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b", "kind": "user", "name": "Alex" },
    { "member_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "kind": "agent", "name": "Alex" }
  ]
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.