Stream events
GET /v1/sessions/{session_id}/events/stream
Streams a session’s events as server-sent events while they happen.
Without Last-Event-ID, the stream sends only events that happen after it opens, so open it before you send a
message.
Each key can hold up to 24 open streams across all its sessions, unless your deployment sets a different limit. The limit applies on each server instance, so a key whose streams land on more than one instance can hold more in total. A stream counts until your client disconnects.
The key must have the tasks:read capability. For what each capability allows, see Keys.
Headers
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
Last-Event-ID | string | No | The ID of the last event you received from this session. The stream replays every event after it, then continues live. sevt_0_0 replays from the start. A send receipt’s ID (sevt_receipt_<uuid>, returned when you send an event) returns 400. |
Path parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
session_id | string | Yes | The ID of the session to stream. |
Query parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
event_deltas | boolean | No | Not supported. Omit it. Any value returns 400. |
Response
Returns 200 with the content type text/event-stream. Each event is one frame, described in Frames. The : open line is a comment, sent once when the stream opens, and SSE clients ignore it.
: open
id: sevt_40_0event: agent.messagedata: {"type":"agent.message","id":"sevt_40_0","processed_at":"2026-10-21T01:42:24.003Z","content":[{"type":"text","text":"The output is `first`."}]}
id: sevt_40_1event: session.usagedata: {"type":"session.usage","id":"sevt_40_1","processed_at":"2026-10-21T01:42:24.003Z","usage":{"input_tokens":6,"output_tokens":82,"cache_read_input_tokens":44428,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"active_seconds":7.288}}
id: sevt_41_0event: session.usagedata: {"type":"session.usage","id":"sevt_41_0","processed_at":"2026-10-21T01:42:24.043Z","usage":{"input_tokens":6,"output_tokens":82,"cache_read_input_tokens":44428,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"active_seconds":7.331}}
id: sevt_41_1event: session.status_idledata: {"type":"session.status_idle","id":"sevt_41_1","processed_at":"2026-10-21T01:42:24.043Z","stop_reason":{"type":"end_turn"},"stop_details":null}
event: pingdata: {"type":"ping"}Errors
This endpoint can return these errors as well as the common errors.
| Status | error.type | Cause and fix |
|---|---|---|
| 400 | invalid_request_error | Last-Event-ID is not an event ID of this session. Send an ID from this session’s events, or omit the header. |
| 400 | invalid_request_error | The request includes the event_deltas query parameter. Remove it. |
| 404 | not_found_error | No session with this ID exists, or this key can’t read it. Check the session ID. |
| 429 | rate_limit_error | This key already holds the most open streams allowed on the server instance that answered, 24 by default. Close a stream you no longer read, then retry. |
Frames
Each event is one frame with id, event and data, and the frame’s id is the event ID. Events arrive
whole, without deltas, and agent.message carries the full reply. Every 20 seconds the server sends a ping
frame with no id.
Event order in a turn
Stop reading when a session.status_idle arrives whose stop_reason.type is anything other than
requires_action. A session.status_idle with requires_action means the agent is waiting for you. A
session.error with retry_status.type: "retrying" doesn’t end the turn, so keep reading. A session.error
with retry_status.type: "exhausted" is followed by a session.status_idle with retries_exhausted.
A turn in which the agent asks to run a tool looks like this:
- The stream shows
user.message, thensession.status_running. - The stream shows
agent.tool_usefor the call, thensession.status_idlewithstop_reason.type: "requires_action"and the call’s ID inevent_ids. - To answer, send a
user.tool_confirmationevent with Send events for each call. The stream shows it, thensession.status_runningonce every waiting call is answered. Until you answer every ID inevent_ids, the session keeps waiting and the stream sends no further status event. - The stream shows
agent.tool_result, thenagent.thinkingif the agent reasoned, thenagent.message. Asession.usageevent follows each model call. - The stream shows
session.usage, thensession.status_idlewithstop_reason.type: "end_turn". The turn is over.
How a stream ends
The server doesn’t close the stream when a turn ends. Close it yourself when you stop reading, because an open stream counts against the key’s limit.
A stream can end with an event: error frame, without an id, whose data is an error body. The server
sends nothing after it, so close the connection yourself.
error.type | Why | What to do |
|---|---|---|
api_error | The connection has carried 8 MiB. | Reconnect with Last-Event-ID. |
invalid_request_error | The session was archived. | Stop. The history stays readable. |
authentication_error | The key was revoked or expired, or its owner left the organization. | Stop. To keep reading, open a new stream with a valid key. |
permission_error, not_found_error | The key lost access to the session. | Stop. To keep reading, open a new stream with a key that can read the session. |
A stream can also close with no frame, for example when the server restarts. The SDKs do not reconnect for you.
Reconnecting
To resume a stream without missing events, follow these steps:
- Keep the
idof the last event you received. - Reopen the stream with a
Last-Event-IDheader set to that ID. The server replays every event after it, then continues live.
If you have not received any event yet, send sevt_0_0 to replay the whole history. To read the history first,
list it with List events and stream from the last listed ID. You
can also open the stream first, then list the history and skip any event IDs the stream has already delivered.
Example
curl -sSN "$CONTEXT_BASE_URL/v1/sessions/dd13d728-93e3-440a-874c-4efeb58c97e0/events/stream" -H "x-api-key: $CONTEXT_API_KEY"
# Resume after the last event you receivedcurl -sSN "$CONTEXT_BASE_URL/v1/sessions/dd13d728-93e3-440a-874c-4efeb58c97e0/events/stream" -H "x-api-key: $CONTEXT_API_KEY" \ -H "Last-Event-ID: sevt_29_0"sid = "dd13d728-93e3-440a-874c-4efeb58c97e0"with client.beta.sessions.events.stream(sid) as stream: for event in stream: print(event.id, event.type) if event.type == "session.status_idle" and event.stop_reason.type != "requires_action": break
# Resume after the last event you receivedstream = client.beta.sessions.events.stream(sid, extra_headers={"Last-Event-ID": "sevt_29_0"})const sid = "dd13d728-93e3-440a-874c-4efeb58c97e0"const stream = await client.beta.sessions.events.stream(sid)for await (const event of stream) { if ("id" in event) console.log(event.id, event.type) if (event.type === "session.status_idle" && event.stop_reason.type !== "requires_action") break}
// Resume after the last event you receivedconst resumed = await client.beta.sessions.events.stream(sid, {}, { headers: { "Last-Event-ID": "sevt_29_0" } })