Events
GET /v1/agents/events, and the event vocabulary shared with the stream.
View as MarkdownReturns what happened in a session since a cursor. Long-polls when nothing is new.
Request
GET /v1/agents/events?sessionId=cs_01HZY&since=eyJ0dXJuIjoxNzkwMDAwMDEyMzQ1fQ&wait=30Response
{
"sessionId": "cs_01HZY",
"status": "running",
"events": [
{ "type": "reasoning-start", "id": "r_1" },
{ "type": "reasoning-delta", "id": "r_1", "delta": "I will open Notepad first." },
{ "type": "reasoning-end", "id": "r_1" },
{ "type": "tool-input-start", "toolCallId": "tc_4", "toolName": "execute" },
{
"type": "tool-input-available",
"toolCallId": "tc_4",
"toolName": "execute",
"input": {
"title": "Opening Notepad and typing the line",
"code": "open('notepad')\ntype('hello')"
}
},
{
"type": "data-progress",
"data": { "text": "Opening Notepad and typing the line", "tool": "execute" }
},
{ "type": "data-progress", "data": { "text": "Notepad window is focused", "tool": "execute" } }
],
"text": "",
"cursor": "eyJ0dXJuIjoxNzkwMDAwMDEyMzQ1LCJzZWVuIjpbInJfMSIsInRjXzQiXX0"
}With approval you do not have to scan events to find what is blocking the task:
{
"sessionId": "cs_01HZY",
"status": "needs_approval",
"events": [
{
"type": "data-approval-request",
"data": {
"approvalId": "ap_7Qw",
"title": "Command Approval Required",
"description": "Sai wants to run a command.",
"approvalType": "exec",
"isLinkOnly": false,
"approvalUrl": "https://sai.simular.ai/approval/3fA9kQ2xYz/ap_7Qw?from=api",
"command": "notepad.exe"
}
}
],
"text": "",
"cursor": "eyJ0dXJuIjoxNzkwMDAwMDEyMzQ1LCJzZWVuIjpbImFwOmFwXzdRdyJdfQ",
"approval": {
"approvalId": "ap_7Qw",
"title": "Command Approval Required",
"description": "Sai wants to run a command.",
"approvalType": "exec",
"isLinkOnly": false,
"approvalUrl": "https://sai.simular.ai/approval/3fA9kQ2xYz/ap_7Qw?from=api",
"command": "notepad.exe"
}
}Each assistant message arrives once, as a start, one delta with the whole message, and an end. A slow poller still sees every message exactly once. A new message from you starts a new turn and resets the cursor; the response then reports the new turn only.
The loop
POST /messagewithwait: false.GET /events?sessionId=...&wait=30.- Act on
status:running, poll again withsince=cursor;needs_approval, see Approvals;idle, readtext;error, read theerrorevent.
Keep wait at 30 to 60 seconds. It is capped at 60 so a single call never approaches the two-minute timeout most tool runners apply.
The request is held while status is running or needs_approval, so a caller that handed a link-only approval to a human can keep long-polling and is woken when the agent resumes. On idle and error it returns at once.
What a caller can see
There are no screenshots on this API. The record of a task is:
- one
data-progressline per step, written by the agent for a human (the tool call'stitle), - the first line of each console message the step printed,
- the final assistant
text.
Sai describes what is on the screen in those lines and in text. If you need to see the computer, open the session in the Sai app.
Errors
Note
GET /eventsis available from October 1. Today's staging has only the SSE path.
Event vocabulary
Both the poll and the stream use these types.
text-delta
Final assistant text. In the poll, one delta carries a whole message; in the stream, concatenate deltas with the same id.
{ "type": "text-delta", "id": "t_1", "delta": "The file is saved." }reasoning-delta
Mid-turn narration: what Sai is about to do. Show it as progress, not as the answer.
{ "type": "reasoning-delta", "id": "r_1", "delta": "I will open Notepad first." }data-progress
A progress line from a running tool. text comes from one of three places:
- the tool call's
title: one line the agent wrote for a human, emitted when the call starts; - the first line of each console message in the tool's result, with accessibility-tree dumps and element-reference listings filtered out, each line capped at 300 characters;
progressLineson a result from the desktop agent (your own computer), passed through as written.
{
"type": "data-progress",
"data": { "text": "Opening Notepad and typing the line", "tool": "execute" }
}tool-input-start
A tool call began. The stream adds toolMetadata; the poll does not.
{ "type": "tool-input-start", "toolCallId": "tc_4", "toolName": "execute" }tool-input-available
Follows tool-input-start with the call's input. For the cloud agent's execute tool it is { title, code }; code is cut at 4000 characters with a note of how much was dropped. On the stream, input is null.
{
"type": "tool-input-available",
"toolCallId": "tc_4",
"toolName": "execute",
"input": {
"title": "Opening Notepad and typing the line",
"code": "open('notepad')\ntype('hello')"
}
}tool-output-error
A tool call failed. Sai usually recovers; the task is not over. errorText is the result's error string, capped at 500 characters, or the last error-level console line when there is none.
{ "type": "tool-output-error", "toolCallId": "tc_4", "errorText": "Window not found" }When the failure is one of the two exhaustion cases below, the event also carries code (and resetsAt for credits):
{
"type": "tool-output-error",
"toolCallId": "tc_9",
"errorText": "Your free cloud computer time is used up for today.",
"code": "free_computer_time_exhausted"
}data-approval-request
Sai is waiting for permission. Check isLinkOnly. Full field list on Approvals.
{
"type": "data-approval-request",
"data": {
"approvalId": "ap_7Qw",
"title": "Command Approval Required",
"description": "Sai wants to run a command.",
"approvalType": "exec",
"isLinkOnly": false,
"approvalUrl": "https://sai.simular.ai/approval/3fA9kQ2xYz/ap_7Qw?from=api",
"command": "notepad.exe",
"cwd": "C:\\Users\\sai",
"expiresAt": 1790000600000
}
}finish
The turn is complete.
{ "type": "finish", "finishReason": "stop" }error
The turn failed.
{ "type": "error", "errorText": "The agent encountered an error. Check the Sai app for details." }When the account ran out of credits, the event carries code and resetsAt:
{
"type": "error",
"errorText": "insufficient_credits: your free credits for today are used up.",
"code": "insufficient_credits",
"resetsAt": "2026-09-30T00:00:00.000Z"
}Error codes
code appears on error and tool-output-error only for these two cases. Everything else has errorText alone. On either code, stop retrying: the same request will fail again today.
GET /account shows the balance and plan behind either error. See Billing and limits.
Framing
Text and reasoning are bracketed by start and end markers on both paths.
{ "type": "text-start", "id": "t_1" }{ "type": "text-end", "id": "t_1" }{ "type": "reasoning-start", "id": "r_1" }{ "type": "reasoning-end", "id": "r_1" }Stream-only frames
The SSE path also emits these. They carry nothing the poll lacks.
{ "type": "start", "messageId": "msg_1" }{ "type": "data-session", "data": { "sessionId": "cs_01HZY" } }{ "type": "data-status", "data": { "text": "Waking the computer" } }The stream ends with the literal line data: [DONE].
Token usage
usage counts the model calls Sai made for the current task, the one your last POST /message started, so you can compare runs. sessionUsage has the same shape and counts every task in the session:
{
"usage": {
"inputTokens": 18450,
"outputTokens": 2210,
"cacheReadTokens": 96300,
"cacheWriteTokens": 0,
"costUsd": 0.31,
"modelCalls": 14,
"byModel": {
"claude-opus-5-5": {
"inputTokens": 18450,
"outputTokens": 2210,
"cacheReadTokens": 96300,
"cacheWriteTokens": 0,
"costUsd": 0.31,
"modelCalls": 14
}
}
}
}- A message sent while a task is running steers it and keeps counting into the same task. The next task starts from zero.
- Only model calls are counted, not computer time.
- Tasks started before usage reporting shipped (October 1) have no
usage. - Each task is counted on its own, so runs in the same session compare fine. A new session also keeps earlier tasks out of Sai's context, which keeps their cost out of the next run's input tokens.