POST /responses is cheaper
to reason about and stores nothing that was said.
Open one
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
external_ref — your own id for the thread
external_ref — your own id for the thread
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.Idempotency-Key — for when there is no external ref
Idempotency-Key — for when there is no external ref
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
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
conversations:read. Messages come back oldest first.
Paging
This endpoint returnsnext_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.
Billing and the channel
A conversation opened over the Runtime API lives on theapi 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.

