Partner chat event cursors
Run events use Server-Sent Events (SSE):
GET /v1/orgs/:orgId/chat/conversations/:id/runs/:runId/events
Authenticate with a server-chat credential with chat:read, and send the conversation's externalUserId and externalTeamId as query parameters. Each frame has id: <runId>:<sequence>, event: <type> and JSON data.
Public run event contract
The SSE event equals JSON data.type. Payloads are flat, with no data envelope.
| event | JSON data |
|---|---|
| turn | {type:"turn",turnId:string,ts:ISO8601} |
| text | {type:"text",text:string} (append delta) |
| tool_call | {type:"tool_call",name:string,id:string,detail?:string} |
| tool_result | {type:"tool_result",name:string,ok:boolean,error?:string}; no raw tool arguments/results |
| done | `{type:"done",stopReason:"completed" |
| run_status | `{type:"run_status",status:"completed" |
| error | {type:"error",code:string}; safe execution code (for example CHAT_EXECUTION_FAILED); poll run for status |
done precedes run_status when the turn ends, including max_iterations. Internal reasoning (thinking), raw tool arguments/results, and tool details are not exposed. Tool errors contain only TOOL_FAILED. Cancellation or an execution failure need not produce done; use the run status as authority. The internal conversation journal retains its own turn.started, text.delta, tool.started, tool.finished vocabulary.
Replay
| Last-Event-ID | Result |
|---|---|
| Omitted | Replay stored events starting at sequence 1, then follow the active run. |
<runId>:0 | Exactly the same: read from the beginning. |
<runId>:N | Replay only events with sequence greater than N, then follow the active run. |
A completed run still replays its stored events, including the final run_status, before closing the connection. Omitting the header does not mean “new events only”. A cursor at the final sequence of a completed run returns an empty stream. Reconnecting does not execute the run again.
The suffix must contain decimal digits representing a nonnegative safe integer (at most 9007199254740991). Negative values, fractions, exponential notation, non-numeric or empty suffixes, and another run's ID return HTTP 400 CHAT_INVALID. Persist the last received event ID to resume without replaying already consumed frames.
Conversation event pagination uses the same cursor rules:
GET /v1/orgs/:orgId/chat/conversations/:id/events?after=<id>:N
This endpoint requires chat:conversation:read. Omit after or use <id>:0 to read from the beginning; <id>:N reads after N. Follow nextCursor to read subsequent pages. Invalid conversation cursors return HTTP 400 INVALID_ARGUMENT. Conversation sequences belong to the conversation, whereas run sequences belong to the run; their cursors are not interchangeable.
Run outcome and step errors
A turn ending with done.stopReason: completed is completed, even after failed tool steps. Only a turn ending on tool_error fails with TOOL_REFUSED; max_iterations remains limited. Use run_status or run polling as outcome authority. Both include toolErrors: [{toolCallId, name, errorCode}], empty when no tool failed. errorCode is the safe TOOL_FAILED, never raw arguments or results. A strict client may treat any nonempty toolErrors as unsuccessful.
The GET run object exposes the same toolErrors list as terminal run_status. Tool call identifiers refer to public tool_call.id. The internal conversation journal keeps its own vocabulary.
Terminal run_status includes the same timings snapshot as the run object, alongside toolErrors. Historical unmeasured snapshots remain null.
Terminal run_status.usage and GET run expose the same immutable ledger cost. See turn usage and cost for exact decimal amounts, model/kind breakdowns, priceSide: cost, and null semantics.
Parts of a history turn
GET …/conversations/:conversationId/messages keeps its newest-first pagination and the existing flattened text unchanged (the API calls this field text, not content). Assistant entries additionally carry optional parts, in journal order:
[{"type":"text","text":"Checking the store."},{"type":"tool_call","toolCallId":"call-1","name":"store_status","status":"succeeded"},{"type":"text","text":"The store is available."}]Adjacent text deltas become one text part; tool calls separate text parts. Text preserves journal whitespace, including separators between model steps. The last text part is the answer when the turn reaches an answer. Earlier text parts are progress notes. Interrupted or limited turns may have only progress text: use the run status to distinguish that case. There is no duplicate answer field, so the ordered journal remains the single source for this view.
Tool status is succeeded, failed, or cancelled; raw arguments, results and private thinking are never included. An unsuccessful tool does not by itself mean the entire run failed. Legacy messages without a journal omit parts; display their unchanged text as a fallback, without guessing boundaries. parts is also omitted while a journal tool lacks its terminal event. User entries omit parts.
The server fetches journal events for the assistant turn IDs in a page with one query, scoped to the conversation, independently of the number of messages. It does not split stored text or make per-message journal reads. Pagination and attachment loading are unchanged. This history projection is independent of the seven-day SSE replay window.