Core concepts

Sessions

One conversation, from open to end — and why your code never holds the transcript.

A session is one conversation. It carries the transcript, the tool calls, the timings, and whatever you knew about the person before it started.

You open one, send turns to it, and end it.

Your code holds no conversation state#

This is the single most important thing on this page.

You send one message. Not the history, not the previous replies, not a growing array that you are responsible for trimming before it exceeds a context window.

JSON
{ "session_id": "c31f9a70-…", "message": "And the cheapest?" }

The stored transcript is the authority. We load it, append your message, run the turn, and store the result.

Three consequences worth stating:

Your client can be trivial. A session id in a cookie or a database row is the whole of it. No serialising message arrays into local storage, no reconciling them after a reconnect.

Conversations survive anything. A user reloads the page, switches from laptop to phone, or comes back tomorrow — the conversation continues from the same session id, because nothing about it lived in the browser.

History cannot be rewritten. Nobody can post a fabricated exchange and then ask the assistant to act on what "it already agreed to", because what it already said is on our side, not in the request body.

Opening one#

Terminal
POST /v1/assistants/{assistant_id}/sessions
JSON
{
  "channel": "chat",
  "visitor": { "name": "Sara", "email": "sara@example.com" },
  "metadata": { "plan": "pro", "referrer": "pricing-page" }
}
Field
channelchat | voiceWhich door this started at. Defaults to chat, and updates itself as the conversation moves between typing and speaking.
visitorobjectAnswers to the assistant's pre-chat form. Keys must match its field keys.
metadataobjectYour own labels, returned on the session and visible in the dashboard.

visitor#

Only keys the assistant's pre-chat form declares are stored; anything else is discarded rather than rejected, so an extra field in your payload is not an outage.

What this buys you is a follow-up-able record. Without it, Assistant history shows an anonymous transcript; with it, it shows a name, an email and everything they said.

It also affects the conversation: an assistant that has been given an email address will not stop to ask for one.

metadata#

Free-form, up to 16 keys. Yours to use for whatever you need to correlate a conversation with your own systems — a user id, a plan tier, an experiment arm, the page it started on.

Not shown to the assistant and never part of the prompt. It is a label on the record, not context for the model.

Warning

Do not put anything sensitive in metadata. It is stored in plain text and shown in the dashboard. Reference your own records by id rather than copying their contents in.

Sending turns#

Terminal
POST /v1/assistants/{assistant_id}/chat

Covered in full under Streaming and the Chat reference.

Turns are sequential within a session. Two overlapping requests for the same session return 409 — the second would be answering a question the first has not finished, and both replies would then be wrong. Wait for the stream to close.

Ending one#

Terminal
POST /v1/sessions/{session_id}/end
JSON
{ "reason": "visitor_left" }

Accepted reasons are visitor_left, assistant_ended and handoff. Anything else is recorded as visitor_left, so the outcome column stays trustworthy — your integration cannot label its own conversations a successful handoff.

Ending is idempotent. Calling it twice is not an error, and the first reason is the one that sticks.

If you never call it#

Nothing breaks. A session that goes quiet is closed automatically, and turns after a time limit return 409.

Ending deliberately buys accuracy: a real duration instead of an inferred one, and an outcome instead of a silence. If you can call it — a beforeunload beacon, a cleanup handler, a job that sweeps stale rows — call it.

Lifecycle#

Code
open ──→ active ──→ ended
              │
              ├──→ expired    ran past its time limit
              └──→ blocked    stopped by a limit

A session only accepts turns while active. Anything else returns 409, and the right response is to open a new one.

Time limits#

Each assistant has a maximum conversation length, set in Studio. Two things follow from it:

Chat responses carry the remaining time. The done event includes remaining_seconds, so a long-running interface can warn before it is cut off.

The assistant knows too. As the limit approaches, it is told to start wrapping up — so a conversation ends with a closing line rather than mid sentence.

Reading a conversation back#

Terminal
GET /v1/sessions/{session_id}

Returns the full transcript: every turn in order, with tool calls, timings, and which turns were spoken rather than typed.

Useful for a "conversation history" screen, for quality review, for feeding a completed conversation into your own systems, and for debugging a turn that did not go the way you expected.

Ordering is guaranteed. Turns carry a sequence number assigned on write, so a transcript reads in the order it happened even when two turns land in the same millisecond.

Retention#

Transcripts are kept for the retention window on the assistant, then deleted. Default is 90 days; 0 means keep indefinitely.

Usage totals outlive transcripts. A conversation ageing out does not change last month's request count.

Next#