Docs/Events
API reference

Events

GET /v1/agents/events, and the event vocabulary shared with the stream.

View as Markdown

Returns 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=30
QueryRequiredNotes
sessionIdyesFrom POST /message. Must belong to the caller, else 403.
sincenoThe cursor from the previous response. Opaque. Omit on the first call.
waitnoSeconds to hold the request open when nothing is new. Default 0, maximum 60.

Response

json
{
  "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"
}
FieldNotes
sessionIdEchoed back.
statusrunning, needs_approval, idle or error.
eventsNew events since since, in order. Same shapes as the SSE frames, without start, data-session, data-status and [DONE].
textSai's final assistant text for the current turn so far. On idle, this is the answer.
cursorOpaque base64url string. Pass as since next time. Examples on this site are illustrative.
approvalPresent when status is needs_approval: the first pending approval, same shape as data-approval-request.data.
usageThis task's model usage so far. See Token usage. Absent until Sai's first model call in the task.
sessionUsageThe same numbers for the whole session so far, across its tasks. Absent alongside usage.

With approval you do not have to scan events to find what is blocking the task:

json
{
  "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

  1. POST /message with wait: false.
  2. GET /events?sessionId=...&wait=30.
  3. Act on status: running, poll again with since=cursor; needs_approval, see Approvals; idle, read text; error, read the error event.

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-progress line per step, written by the agent for a human (the tool call's title),
  • 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

StatusBody
400{ "error": "sessionId is required." }
403{ "error": "Session not found or does not belong to your account." } for a missing session and for another account's session alike.
429{ "error": "Event poll rate limit exceeded. Use wait= to long-poll." } after 1200 calls in an hour.

Note

GET /events is 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.

json
{ "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.

json
{ "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;
  • progressLines on a result from the desktop agent (your own computer), passed through as written.
json
{
  "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.

json
{ "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.

json
{
  "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.

json
{ "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):

json
{
  "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.

json
{
  "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.

json
{ "type": "finish", "finishReason": "stop" }

error

The turn failed.

json
{ "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:

json
{
  "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.

codeMeaningresetsAt
insufficient_creditsNo credit left to spend. A free account's daily credits refresh at UTC midnight; a paid account tops up in the portal.The next UTC midnight, ISO 8601.
free_computer_time_exhaustedThe free plan's daily computer time is spent. The workspace was shut down. Resets daily; the exact local time is shown in the Sai app.Absent.

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.

json
{ "type": "text-start", "id": "t_1" }
json
{ "type": "text-end", "id": "t_1" }
json
{ "type": "reasoning-start", "id": "r_1" }
json
{ "type": "reasoning-end", "id": "r_1" }

Stream-only frames

The SSE path also emits these. They carry nothing the poll lacks.

json
{ "type": "start", "messageId": "msg_1" }
json
{ "type": "data-session", "data": { "sessionId": "cs_01HZY" } }
json
{ "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:

json
{
  "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
      }
    }
  }
}
FieldNotes
inputTokensUncached input tokens.
outputTokensOutput tokens, reasoning included.
cacheReadTokens, cacheWriteTokensPrompt-cache reads and writes, where the model reports them.
costUsdWhat the calls cost your credits.
modelCallsModel calls made in the task.
byModelThe same numbers per model id.
  • 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.