Skip to main content
A conversation is a durable thread. Use one when the transcript matters — when the next turn should see the previous ones, or when a person will read the history later. If neither is true, POST /responses is cheaper to reason about and stores nothing that was said.

Open one

Needs conversations:create. A conversation is bound to its tenant, environment and principal at creation rather than acquiring a binding later. A window in which there is nothing to check is a window in which every check passes.

Two ways not to create a duplicate

Resolving the same external_ref twice returns the same conversation rather than a second one beside it. That is what a CRM retrying a webhook needs, and it survives long past any idempotency window.The uniqueness is scoped by (environment, tenant, external_ref) and by nothing less. A blank ref resolves to nothing at all — a missing public identity must never resolve to a real one.Printable, no newlines, up to 200 characters.
The ordinary 24-hour replay contract. Covers the retry that happens before your side has an id to hand us.A duplicate conversation is a duplicate transcript and a split history, so this endpoint is one of the ones where sending a key is worth the trouble. Idempotency

Append a turn

Needs conversations:write. Same options and same response shape as /responses — including "stream": true.
The body does not name an agent. It is taken from the conversation. A body that named a different one would be a request to run somebody else’s assistant on this transcript.

Read it back

Both need conversations:read. Messages come back oldest first.

Paging

This endpoint returns next_cursor and reads it back as after — not cursor, which is what the document and extraction endpoints use. Its cursor is a message id rather than an encoded pair.
limit defaults to 50 and is capped at 100 — a ceiling as well as a default, because “give me every message” on a two-year-old conversation is a request that succeeds slowly and then fails. Ordering is on (created_at, id) and paging is on the same pair. A timestamp alone is not stable: two messages written in the same millisecond would make a page boundary ambiguous, and an ambiguous boundary is a message silently skipped or repeated.
An after cursor that names nothing in this conversation is a 400, not an empty page. Silently returning the first page would make a paging client loop forever.

Billing and the channel

A conversation opened over the Runtime API lives on the api channel. That is a slug of its own and not an alias for the website: Runtime API spend is invoiced and reported as itself rather than folded into another channel’s column. You can override it per turn with channel in the input — web, website, whatsapp, phone, voice — when the Runtime API is carrying traffic that really did arrive somewhere else. "" means “not stated” and falls through to the conversation’s own channel.

Not found means not found

The five-check binding gate runs before a single field is read. A guessed id gets the same 404 whether it names another workspace’s conversation or nothing at all — byte for byte. There is no 403 that confirms existence.

What a conversation stores

Rows recording what was said, and nothing about how the model was prompted. What the model is shown is assembled at execution time and is not part of the conversation state, so it is not readable back and not caller-settable. Structured output is written to the transcript only after it validates. Output that failed its schema never reaches the transcript at all — see Structured outputs.