Skip to main content

Overview

/v1/rooms lets a backend, script or agent work with team 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.
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.
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 when that changes.
Authenticate as on every /v1 route (Authentication). A browser session is refused with 401 AUTHENTICATION_REQUIRED.

Find a room

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

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

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

The response is text/event-stream. Each frame is event: <type> and data: <JSON>; the JSON repeats type. 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.
1

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

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

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.
Node.js
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 per account, across all rooms.
  • The general account limit 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

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.