> ## Documentation Index
> Fetch the complete documentation index at: https://docs.simular.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

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

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
```

| Query | Required | Notes |
| - | - | - |
| `sessionId` | yes | From `POST /message`. Must belong to the caller, else `403`. |
| `since` | no | The `cursor` from the previous response. Opaque. Omit on the first call. |
| `wait` | no | Seconds to hold the request open when nothing is new. Default `0`, maximum `60`. |

## Response

```json theme={null}
{
  "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"
}
```

| Field | Notes |
| - | - |
| `sessionId` | Echoed back. |
| `status` | `running`, `needs_approval`, `idle` or `error`. |
| `events` | New events since `since`, in order. Same shapes as the SSE frames, without `start`, `data-session`, `data-status` and `[DONE]`. |
| `text` | Sai's final assistant text for the current turn so far. On `idle`, this is the answer. |
| `cursor` | Opaque base64url string. Pass as `since` next time. Examples on this site are illustrative. |
| `approval` | Present when `status` is `needs_approval`: the first pending approval, same shape as `data-approval-request.data`. |

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

```json theme={null}
{
  "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](/sai-api/concepts/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

| Status | Body |
| - | - |
| `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.
</Note>

## 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 theme={null}
{ "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 theme={null}
{ "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 theme={null}
{ "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 theme={null}
{ "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 theme={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.

```json theme={null}
{ "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 theme={null}
{
  "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](/sai-api/concepts/approvals).

```json theme={null}
{
  "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 theme={null}
{ "type": "finish", "finishReason": "stop" }
```

### `error`

The turn failed.

```json theme={null}
{ "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 theme={null}
{
  "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.

| `code` | Meaning | `resetsAt` |
| - | - | - |
| `insufficient_credits` | No 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_exhausted` | The 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](/sai-api/concepts/billing-and-limits#when-you-run-out).

### Framing

Text and reasoning are bracketed by start and end markers on both paths.

```json theme={null}
{ "type": "text-start", "id": "t_1" }
```

```json theme={null}
{ "type": "text-end", "id": "t_1" }
```

```json theme={null}
{ "type": "reasoning-start", "id": "r_1" }
```

```json theme={null}
{ "type": "reasoning-end", "id": "r_1" }
```

## Stream-only frames

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

```json theme={null}
{ "type": "start", "messageId": "msg_1" }
```

```json theme={null}
{ "type": "data-session", "data": { "sessionId": "cs_01HZY" } }
```

```json theme={null}
{ "type": "data-status", "data": { "text": "Waking the computer" } }
```

The stream ends with the literal line `data: [DONE]`.
