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:
https://api.sai.simular.aiAgent 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 withsapi key generateor 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.
curl https://api.sai.simular.ai/v1/agents/auth \
-H "Authorization: Bearer sapi_your_api_key_here"{ "ok": true, "userId": "abc123", "authType": "apiKey" }/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, exceptPOST /v1/agents/upload(raw binary) andPOST /v1/agents/message(an SSE response stream). - Ownership — every
machineIdis verified against the authenticated user. Passing another user’s machine returns403or404. - Errors — non-2xx responses return
{ "error": "…" }. See error responses.
Verify the caller’s credentials and return their identity. Useful as a health check before storing a credential.
{ "ok": true, "userId": "abc123", "authType": "apiKey" }List all machines registered to the user, sorted by name.
{
"machines": [
{ "machineId": "m_1a2b", "name": "MacBook Pro", "updatedAt": 1750000000000 }
]
}Resolve the user’s most-recently-active machine and its active session. Returns 404 if no machine is registered.
{ "machineId": "m_1a2b", "sessionId": "s_9z8y" }Switch the active session on a machine (resume an earlier conversation).
Body
{ "machineId": "m_1a2b", "sessionId": "s_9z8y" }{ "sessionId": "s_9z8y" }List the 20 most-recent sessions for a machine, newest first. Requires a machineId query parameter.
curl "https://api.sai.simular.ai/v1/agents/sessions?machineId=m_1a2b" \
-H "Authorization: Bearer sapi_..."{
"sessions": [
{ "sessionId": "s_9z8y", "title": "Email action items", "updatedAt": 1750000000000, "active": true }
]
}Start a fresh conversation on a machine and make it active.
Body
{ "machineId": "m_1a2b" }{ "sessionId": "s_new123" }Fetch recent messages from the machine’s active session. Query params: machineId (required) and limit (default 30, max 100).
{
"messages": [
{ "role": "user", "content": "list my unread emails", "timestamp": 1750000000000 },
{ "role": "assistant", "content": "You have 3 unread emails...", "timestamp": 1750000005000 }
]
}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.
curl -X POST https://api.sai.simular.ai/v1/agents/upload \
-H "Authorization: Bearer sapi_..." \
-H "x-filename: workflow.sim" \
--data-binary @./workflow.sim{
"path": "uploads/workflow-1750000000000.sim",
"name": "workflow.sim",
"mime": "text/plain",
"size": 1024,
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/.../o/...?alt=media&token=..."
}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
{
"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
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)
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.
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
{ "approvalId": "ap_123", "response": "yes" }response is one of yes, no, or always (only valid for non-high-risk shell commands).
{ "ok": true, "status": "approved" }Abort the running task on a machine. Idempotent — safe to call when nothing is running.
Body
{ "machineId": "m_1a2b" }{ "ok": true, "aborted": true }
// or, when idle:
{ "ok": true, "aborted": false, "reason": "no active session" }Restart the agent process or the full cloud machine. Returns 422 if restart is not supported for the workspace type.
Body
{ "machineId": "m_1a2b", "target": "agent" }target is agent (just the Sai process) or machine (full VM, slower).
{ "ok": true, "action": "restarted" }Generate a long-lived API key. The plaintext key is returned once — only its hash is stored. Maximum 10 keys per account.
Body
{ "name": "ci-pipeline" }{ "key": "sapi_AbC123...", "keyId": "k_456", "name": "ci-pipeline" }List API keys for the authenticated user (IDs and labels only — never the plaintext secret).
{
"keys": [
{ "keyId": "k_456", "name": "ci-pipeline", "createdAt": 1750000000000, "lastUsedAt": 1750000500000 }
]
}Revoke an API key by ID. Returns 204 No Content on success.
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 type | Meaning |
|---|---|
start | Stream opened. |
data-concierge | Routing decision — category is task, context, or steer — with its latency in llmMs. |
data-status | Informational status line (data.text). |
text-start / text-delta / text-end | The agent’s final response text, streamed in deltas. |
reasoning-start / -delta / -end | Mid-turn narration emitted between tool calls. |
tool-input-start | A tool invocation began (toolName, toolCallId, toolMetadata.retrying). |
tool-input-available | Tool input is ready. |
data-progress | A progress line from a running tool (data.text, data.tool). |
tool-output-error | A tool errored (errorText). The task may still retry or recover. |
data-approval-request | The agent needs permission. Resolve via POST /v1/agents/approve using the approvalId. |
finish | Response complete (finishReason). |
error | An error occurred (errorText). |
: keepalive) every 20 seconds during long tasks so proxies don’t close idle connections. Ignore comment lines in your parser.Rate limits
| Action | Limit |
|---|---|
| Send a message | 60 / hour |
| Upload a file | 30 / hour |
| Abort a task | 20 / hour |
| Restart agent or machine | 10 / hour |
| Generate an API key | 5 / 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" }.
| Status | Meaning |
|---|---|
400 | Bad request — missing or invalid parameters. |
401 | Missing, invalid, or expired credential. |
403 | User not active, machine not owned, or API key used on a Firebase-only endpoint. |
404 | Machine, session, or key not found. |
409 | Approval request is no longer pending. |
413 | Uploaded file exceeds 25 MB. |
422 | Key limit reached, or restart not supported. |
429 | Rate limit exceeded. |
503 | Auth service temporarily unavailable — retry shortly. |