SAPI · Developer docs

Sai HTTP API

Send tasks to your Sai agent programmatically over HTTPS. The same REST surface that powers the sapi CLI, available for CI pipelines, devices, and custom integrations.

Overview

The Sai HTTP API exposes your agent over HTTPS. It is the same REST surface the sapi CLI is built on — use it directly to integrate Sai into CI pipelines, devices, or your own applications.

All endpoints share a single base URL:

text
https://api.sai.simular.ai

Agent endpoints live under /v1/agents and account (API key) endpoints under /v1/account. All requests and responses are JSON unless noted otherwise.

Authentication

Every request requires a bearer token in the Authorization header. Two credential types are accepted:

  • API key — a long-lived secret prefixed sapi_. Best for servers, CI, and devices. Generate one with sapi key generate or from Settings → API Keys in the desktop app.
  • Firebase ID token — a short-lived token from an interactive Google sign-in. Required for managing API keys.
bash
curl https://api.sai.simular.ai/v1/agents/auth \
  -H "Authorization: Bearer sapi_your_api_key_here"
json
{ "ok": true, "userId": "abc123", "authType": "apiKey" }
API key management endpoints (/v1/account/keys) reject API-key auth — they require a Firebase session so that a leaked key cannot enumerate or revoke other keys.

Conventions

  • Base path — agent routes are under /v1/agents; account routes under /v1/account.
  • Content type — send and receive application/json, except POST /v1/agents/upload (raw binary) and POST /v1/agents/message (an SSE response stream).
  • Ownership — every machineId is verified against the authenticated user. Passing another user’s machine returns 403 or 404.
  • Errors — non-2xx responses return { "error": "…" }. See error responses.
GET/v1/agents/auth

Verify the caller’s credentials and return their identity. Useful as a health check before storing a credential.

json
{ "ok": true, "userId": "abc123", "authType": "apiKey" }
GET/v1/agents/machines

List all machines registered to the user, sorted by name.

json
{
  "machines": [
    { "machineId": "m_1a2b", "name": "MacBook Pro", "updatedAt": 1750000000000 }
  ]
}
GET/v1/agents/session

Resolve the user’s most-recently-active machine and its active session. Returns 404 if no machine is registered.

json
{ "machineId": "m_1a2b", "sessionId": "s_9z8y" }
PATCH/v1/agents/session

Switch the active session on a machine (resume an earlier conversation).

Body

json
{ "machineId": "m_1a2b", "sessionId": "s_9z8y" }
json
{ "sessionId": "s_9z8y" }
GET/v1/agents/sessions

List the 20 most-recent sessions for a machine, newest first. Requires a machineId query parameter.

bash
curl "https://api.sai.simular.ai/v1/agents/sessions?machineId=m_1a2b" \
  -H "Authorization: Bearer sapi_..."
json
{
  "sessions": [
    { "sessionId": "s_9z8y", "title": "Email action items", "updatedAt": 1750000000000, "active": true }
  ]
}
POST/v1/agents/new-session

Start a fresh conversation on a machine and make it active.

Body

json
{ "machineId": "m_1a2b" }
json
{ "sessionId": "s_new123" }
GET/v1/agents/context

Fetch recent messages from the machine’s active session. Query params: machineId (required) and limit (default 30, max 100).

json
{
  "messages": [
    { "role": "user", "content": "list my unread emails", "timestamp": 1750000000000 },
    { "role": "assistant", "content": "You have 3 unread emails...", "timestamp": 1750000005000 }
  ]
}
POST/v1/agents/upload25 MB max30/hour

Upload a file to attach to a message. Send the raw file bytes as the request body and the filename in an x-filename header (URL-encoded). The MIME type is derived server-side from the extension. Call this before POST /v1/agents/message and pass the returned object in attachments.

bash
curl -X POST https://api.sai.simular.ai/v1/agents/upload \
  -H "Authorization: Bearer sapi_..." \
  -H "x-filename: workflow.sim" \
  --data-binary @./workflow.sim
json
{
  "path": "uploads/workflow-1750000000000.sim",
  "name": "workflow.sim",
  "mime": "text/plain",
  "size": 1024,
  "downloadUrl": "https://firebasestorage.googleapis.com/v0/b/.../o/...?alt=media&token=..."
}
POST/v1/agents/messageSSE60/hour

Send a message to the agent. The response is a Server-Sent Events stream (the Vercel AI SDK v6 UI Message Stream protocol) that emits the agent’s narrations, tool activity, approval requests, and final text until the task completes.

Body

json
{
  "machineId": "m_1a2b",
  "message": "list my unread emails from today",
  "attachments": [
    {
      "path": "uploads/workflow-1750000000000.sim",
      "name": "workflow.sim",
      "mime": "text/plain",
      "size": 1024,
      "downloadUrl": "https://firebasestorage.googleapis.com/..."
    }
  ]
}

attachments is optional and accepts the objects returned by /v1/agents/upload.

Request

bash
curl -N -X POST https://api.sai.simular.ai/v1/agents/message \
  -H "Authorization: Bearer sapi_..." \
  -H "Content-Type: application/json" \
  -d '{"machineId":"m_1a2b","message":"list my unread emails"}'

Response (SSE)

text
data: {"type":"start"}
data: {"type":"data-concierge","data":{"category":"task","llmMs":189}}
data: {"type":"reasoning-start","id":"r1"}
data: {"type":"reasoning-delta","id":"r1","delta":"Opening Gmail..."}
data: {"type":"reasoning-end","id":"r1"}
data: {"type":"text-start","id":"t1"}
data: {"type":"text-delta","id":"t1","delta":"You have 3 unread emails: ..."}
data: {"type":"text-end","id":"t1"}
data: {"type":"finish","finishReason":"stop"}
data: [DONE]

See streaming events for the full event catalogue, including approval requests.

POST/v1/agents/approve

Resolve a pending approval surfaced by a data-approval-request event during a message stream. The original stream stays open and the agent continues once resolved.

Body

json
{ "approvalId": "ap_123", "response": "yes" }

response is one of yes, no, or always (only valid for non-high-risk shell commands).

json
{ "ok": true, "status": "approved" }
POST/v1/agents/abort20/hour

Abort the running task on a machine. Idempotent — safe to call when nothing is running.

Body

json
{ "machineId": "m_1a2b" }
json
{ "ok": true, "aborted": true }
// or, when idle:
{ "ok": true, "aborted": false, "reason": "no active session" }
POST/v1/agents/restart10/hour

Restart the agent process or the full cloud machine. Returns 422 if restart is not supported for the workspace type.

Body

json
{ "machineId": "m_1a2b", "target": "agent" }

target is agent (just the Sai process) or machine (full VM, slower).

json
{ "ok": true, "action": "restarted" }
POST/v1/account/keysFirebase only5/hour

Generate a long-lived API key. The plaintext key is returned once — only its hash is stored. Maximum 10 keys per account.

Body

json
{ "name": "ci-pipeline" }
json
{ "key": "sapi_AbC123...", "keyId": "k_456", "name": "ci-pipeline" }
GET/v1/account/keysFirebase only

List API keys for the authenticated user (IDs and labels only — never the plaintext secret).

json
{
  "keys": [
    { "keyId": "k_456", "name": "ci-pipeline", "createdAt": 1750000000000, "lastUsedAt": 1750000500000 }
  ]
}
DELETE/v1/account/keys/:keyIdFirebase only

Revoke an API key by ID. Returns 204 No Content on success.

bash
curl -X DELETE https://api.sai.simular.ai/v1/account/keys/k_456 \
  -H "Authorization: Bearer <firebase-id-token>"

Streaming events

POST /v1/agents/message streams newline-delimited SSE frames (data: {...}\n\n). Each frame is a JSON object with a type. Unknown event types should be ignored for forward compatibility. The stream ends with a literal data: [DONE] sentinel.

Event typeMeaning
startStream opened.
data-conciergeRouting decision — category is task, context, or steer — with its latency in llmMs.
data-statusInformational status line (data.text).
text-start / text-delta / text-endThe agent’s final response text, streamed in deltas.
reasoning-start / -delta / -endMid-turn narration emitted between tool calls.
tool-input-startA tool invocation began (toolName, toolCallId, toolMetadata.retrying).
tool-input-availableTool input is ready.
data-progressA progress line from a running tool (data.text, data.tool).
tool-output-errorA tool errored (errorText). The task may still retry or recover.
data-approval-requestThe agent needs permission. Resolve via POST /v1/agents/approve using the approvalId.
finishResponse complete (finishReason).
errorAn error occurred (errorText).
The server sends an SSE comment (: keepalive) every 20 seconds during long tasks so proxies don’t close idle connections. Ignore comment lines in your parser.

Rate limits

ActionLimit
Send a message60 / hour
Upload a file30 / hour
Abort a task20 / hour
Restart agent or machine10 / hour
Generate an API key5 / hour

Limits are per user on a rolling one-hour window and shared across all clients. Exceeding one returns 429 with a message naming the limit.

Error responses

Errors use standard HTTP status codes with a JSON body of the form { "error": "message" }.

StatusMeaning
400Bad request — missing or invalid parameters.
401Missing, invalid, or expired credential.
403User not active, machine not owned, or API key used on a Firebase-only endpoint.
404Machine, session, or key not found.
409Approval request is no longer pending.
413Uploaded file exceeds 25 MB.
422Key limit reached, or restart not supported.
429Rate limit exceeded.
503Auth service temporarily unavailable — retry shortly.