Skip to main content
Every session has two durable streams: .in carries records from your clients to the task, .out carries records from the task back to your clients. The sessions SDK wraps these as session.in.* and session.out.*. This page documents the underlying HTTP endpoints for callers that aren’t using the TypeScript SDK. All channel endpoints live under /realtime/v1/sessions/{session}/{io}, where:
  • {session} is the session’s friendly ID (session_…) or your externalId. One token authorizes both forms.
  • {io} is either in or out.
Authorize requests with a secret key or a session public token. The token’s scopes decide what you can do — see Authorization below.

Append a record

Append a single record to a channel.
Append to .in
The body is the raw record — any text up to 1MiB (records over the per-record cap return 413). The response is { "ok": true }. Set the X-Part-Id header to a unique value per record to make the append idempotent: replaying the same X-Part-Id does not duplicate the record. Appending to a closed or expired session returns 400.
Appending to .out requires a secret key. A session public token (even one with write:sessions) can only append to .in — appending to .out with a public token returns 403. The .out stream is the task’s to write.

Read a channel over SSE

Subscribe to a channel as a Server-Sent Events stream. New records are delivered as they arrive.
Read .out
Each SSE event carries:
  • id: — the record’s sequence number. Use the most recent one as Last-Event-ID to resume.
  • data: — a JSON record { "data": <record>, "id": <id> }. For .out on a chat.agent session, data is a UI message chunk (text, reasoning, tool call, or a custom data part).

Control records

Some .out events are control records rather than data. A control record has an empty body and carries a trigger-control header naming its subtype: Route control records by their subtype instead of treating them as message content. The TypeScript SDK does this for you — session.out.read filters control records out of the chunk stream and surfaces them through onControl.

Drain records non-streaming

Fetch a batch of records without holding an SSE connection open. Useful for polling or for reading a tail at startup.
Drain .out
Pass afterEventId to return only records after that sequence number; omit it to read from the start of the retained window. The response is:
Each record carries data, id, seqNum, and an optional headers array (present on control records). Page forward by passing the highest seqNum you received as the next afterEventId.

Authorization

The action you can take depends on your token and the channel: Reads work in both directions for a read:sessions token. Writes split by direction: a write:sessions token can append to .in, but .out is reserved for the task and requires a secret key. See session scopes for how to mint a token.

Using the SDK instead

If you’re writing TypeScript, the sessions SDK is the ergonomic path. sessions.open(idOrExternalId) returns a SessionHandle whose session.in and session.out channels call these endpoints for you, with auto-retry, Last-Event-ID resume, and control-record routing built in:
Your backend
See session.in and session.out for the full handle API.