Conversations and messages
Developer·3 minutes to read
What it does
The conversations the key's person is in, and the messages inside one: read a thread, page through it, and read the cards each message carries (the sidebar row, the composer, the append-only transcript, the card catalogue, the five-part result, and the per-message request id).
Authentication
An organisation API key. The reads need scope read or wider. The two writes need write or wider and an Idempotency-Key, and answer 201: POST /conversations with { "botId": "…" } opens a new session with one of the person's bots and answers the conversation, and POST /conversations/{id}/messages with { "message": "…" } (1 to 32 000 characters) sends a message there and starts a run, answering { run, conversationId, messageId, messageSeq } exactly as POST /runs does (Runs); a run already live in that conversation is 409 conflict with details.reason run_active. Conversations are read under the key's person, and the product shows a person only their own: a group conversation they are not in is not_found here, exactly as it is absent there. See Authentication and keys.
Request
GET /conversations
<!-- generated:begin GET /conversations --> <!-- generated:end GET /conversations -->GET /conversations/{id}
<!-- generated:begin GET /conversations/{id} --> <!-- generated:end GET /conversations/{id} -->GET /conversations/{id}/messages
<!-- generated:begin GET /conversations/{id}/messages --> <!-- generated:end GET /conversations/{id}/messages -->GET /conversations takes an optional botId and lists the person's own direct and group conversations. GET /conversations/{id}/messages takes limit and cursor like every list, and also afterSeq: pass the sequence number you last read and the page starts after it, oldest first. That is the shape to poll a thread with.
Message bodies are the app's own redacted projections. This API adds no unredacted reader, and the run ledger — the hash-chained record of model and tool calls — is not exposed here at all.
curl -sS "$BOTSEON_API_URL/api/v1/conversations?limit=20" \
-H "authorization: Bearer $BOTSEON_API_KEY" \
-H "accept: application/json"
const origin = process.env.BOTSEON_API_URL;
const auth = { authorization: `Bearer ${process.env.BOTSEON_API_KEY}`, accept: 'application/json' };
const list = await fetch(`${origin}/api/v1/conversations?limit=1`, { headers: auth });
if (!list.ok) throw new Error(`conversations: ${list.status}`);
const page = (await list.json()) as { data: Array<{ id: string; title: string | null }> };
const first = page.data[0];
if (first) {
const messages = await fetch(
`${origin}/api/v1/conversations/${first.id}/messages?afterSeq=0&limit=50`,
{ headers: auth },
);
if (!messages.ok) throw new Error(`messages: ${messages.status}`);
const thread = (await messages.json()) as { data: Array<{ seq: number; role: string }> };
for (const message of thread.data) console.log(message.seq, message.role);
}
Response
{
"data": [
{
"id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
"seq": 41,
"role": "member",
"kind": "message",
"body": { "cards": [{ "kind": "message", "text": "Here are the release notes." }] },
"runId": null,
"cards": [],
"requestId": "…",
"authorUserId": "8c1d4e5f-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
"botId": null,
"createdAt": "2026-09-18T08:00:00.000Z"
}
],
"nextCursor": null
}
seq is the message's place in the thread and is what afterSeq takes. requestId is the id the app shows beside a message, and quoting it in a support request is what makes an answer possible.
Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | unauthenticated | The credential is missing, refused, or below read. | Check the key. |
| 404 | not_found | No such conversation, or the key's person is not in it. | List first; a group the person is not a member of never appears. |
| 400 | invalid_request | limit, cursor or afterSeq is outside its bounds, a path id or botId is not a UUID, a message is empty or over 32 000 characters, or a POST carries no Idempotency-Key. | limit is 1 to 200; afterSeq is a non-negative integer. |
| 409 | conflict | POST /conversations/{id}/messages while a run is live in that conversation. | Wait for Retry-After, then retry with the same Idempotency-Key. |
| 429 | rate_limited | A bucket is full. | Wait for Retry-After. |
Rate limits
Every call on this page is counted against two one-minute buckets: 300 requests per key and 1 000 per organisation. A full bucket answers rate_limited with Retry-After: 60; wait that many seconds and retry. Failed authentication is counted separately, against the caller's address, so a working key's allowance is never spent by somebody else's bad credential. See Envelopes, errors, idempotency and paging.
Last verified against build c0f77aa.