HTTP API

Shardflux HTTP API reference. Base URL, /v1 versioning, API keys, people's sessions, tool tokens, idempotency, pagination, operations, rate limits and cells.

Overview

The Shardflux API has two parts:

Part Base URL Authorization Serves
Application API https://api.shardflux.dev/v1 Authorization: Bearer sfk_<key id>_<secret> (a project API key), or Authorization: Bearer sfu_<token> (a person's session) Workspaces and their lifecycle, operations, templates, secrets, volumes, egress, usage; with a person's session also the account, organizations, members, API keys and billing
Cell gateway the workspace's cell_endpoint, /v1/workspaces/{workspace_id}/... Authorization: Bearer <tool token> Workspace tools: exec, terminals, processes, files, git, browser

Every application API endpoint an API key can call is in Endpoints, generated from the OpenAPI document at https://docs.shardflux.dev/openapi.json. The cell gateway's endpoints are listed in Cell endpoints below. The TypeScript SDK, Python SDK and CLI call this API and handle retries, idempotency keys, operation waits and tool tokens for you.

Versioning

All routes are under /v1. Within /v1, new fields, enum values, error codes and details.reason values can appear at any time: ignore fields you do not know, and treat an unknown error code or reason as a generic error (show message, use retryable).

GET /v1/client-versions (no authentication, cached for an hour) names, for every Shardflux client package, the latest published version and the oldest one this API still supports:

JSON
{ "clients": [{ "package": "@shardflux/cli", "ecosystem": "npm", "latest": "0.16.0", "minimum_supported": "0.1.0", "upgrade_command": "npm i -g shardflux", "release_notes_url": "https://www.npmjs.com/package/@shardflux/cli?activeTab=versions", "hold": null }] }

The CLI (0.5.0+), the TypeScript SDK (0.9.0+), the Python SDK (0.5.0+) and the MCP server (0.4.0+) read it to tell you when to update. latest is null for a package with no published release. The CLI (0.15.2+), the TypeScript SDK (0.22.0+), the Python SDK (0.18.0+) and the MCP server (0.15.0+) take the latest release from npm or PyPI and use this latest when the registry does not answer; hold, when set, names a release they do not recommend.

Authentication

API keys

A project API key has the form sfk_<key id>_<secret>. The key id is public; the secret is shown once, when you create the key in the console. Send it on every application API request:

Shell
curl -sS https://api.shardflux.dev/v1/me -H "Authorization: Bearer $SHARDFLUX_API_KEY"
  • An API key belongs to one project. Workspaces, secrets and volumes it creates are in that project; other projects' resources answer 404 not_found.
  • A key carries tool permissions, a subset of exec, files, pty, process, git and browser, which bound what its tool tokens may do. GET /v1/me returns the key's organization, project and tool_permissions.
  • An API key is a server credential. Keep it out of browsers and client-side code.
  • A missing or non-Bearer Authorization header, and a malformed, unknown, revoked or expired key, are 401 unauthenticated. Cookies are ignored.

A person's session

The second credential on /v1 is a person's CLI session, sfu_ followed by 43 characters. The shard CLI (0.5.0+) and the SDKs' ShardfluxAccount (TypeScript 0.9.0+, Python 0.5.0+) sign in with it, so everything a person does in the console works without a browser. Use them rather than these routes directly.

Shell
curl -sS https://api.shardflux.dev/v1/auth/session -H "Authorization: Bearer $SHARDFLUX_SESSION_TOKEN"
  • Signing up from an agent. POST /v1/auth/signup answers with a session at once (201 created, 200 signed in): {status, created, access, user, session_token, session_expires_at}. With email only, a new account has access: provisional: every /v1 route works on the default plan except Checkout, the billing portal, invoices, spend policy, invitations and credential changes, which need a verified email. POST /v1/auth/verify-email/code with {} emails a 6-digit code and with {code} verifies the address; nothing is signed out. Terms: accept_terms: true on signup and register says the person agreed to the Terms of Service and acknowledged the Privacy Policy; a new account records when, and which version (terms_version, the effective date YYYY-MM-DD; default: the current one).
  • Signing in. /v1/auth has the console's sign-in routes: register, verify-email (and verify-email/resend), login, mfa/challenge, session, logout, logout-all, sessions, step-up, password/change, password/reset/request, password/reset/confirm, email/change, email/change/confirm and the mfa/totp and mfa/recovery-codes routes. GET /v1/auth/identities lists the account's sign-in methods (has_password, and the linked Google and GitHub identities); DELETE /v1/auth/identities/{provider} removes one after a step-up (409 conflict with details.reason last_sign_in_method when it is the only one). Google and GitHub sign-in run in the console. Where the console gets a cookie, the answer carries session_token (sfu_...) and session_expires_at. Emailed links are the console's (https://app.shardflux.dev/auth/verify-email#token=...): post the token of the link.
  • Rotation. Completing a sign-in, a step-up, a password change and turning two-factor authentication on or off return a new session_token and revoke the previous one.
  • Lifetime. A session lasts 30 days from its last use and at most 90 days. The person sees it in the console (Settings > Account & security) and can sign it out there; POST /v1/auth/logout ends it.
  • What it can do. Every /v1 route takes it as the person: their memberships and roles decide, and routes that need a project take project_id. Account routes (account and organization deletion and exports, the audit export, Checkout, the billing portal, invoices, spend alerts, template publish and archive, members, invitations and API keys) accept only a person's session; an API key gets 403 forbidden there.
  • Refusals. An unknown, revoked or expired session is 401 unauthenticated. A session waiting for its second factor gets 403 mfa_required, an unverified email 403 email_unverified, and a sensitive action without a recent step-up 403 step_up_required. See Errors.
  • A project API key is not a session: the /v1/auth routes that need a session answer it with 401.

Tool tokens

The cell gateway does not accept API keys. It accepts workspace tool tokens: short-lived (at most 15 minutes) ES256 tokens bound to one workspace, one set of tools and the workspace's current ownership epoch.

Shell
curl -sS -X POST "https://api.shardflux.dev/v1/workspaces/$WORKSPACE_ID/tool-tokens" \
  -H "Authorization: Bearer $SHARDFLUX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_label": "my-agent", "tools": ["exec", "files"]}'
Request field Meaning
agent_label Optional attribution label, 1-100 characters. One agent session per workspace, principal and label.
tools Optional subset of the key's tool permissions. Asking for more is 403 forbidden with details.denied_tools.

The response (ToolToken) has token, token_type (Bearer), issued_at, expires_at, workspace_id, agent_session_id, ownership_epoch, tools, audience, kid and cell_endpoint. Send token to cell_endpoint.

  • A token works for tool calls while the workspace runs. A suspended workspace gets a token too: with it the cell answers only reads of the workspace's disk (GET .../files, files/stat, files/list, POST .../files/search, with X-Served-From: disk) and every other call with 409 workspace_not_running. While a suspend or a requested resume is in progress, the request is 409 conflict with details.reason: workspace_not_running and the operation; a deleted workspace is 409 conflict workspace_deleted.
  • After a suspend, resume or move the workspace's epoch changes and the cell answers 409 stale_epoch: get a new token and reconnect.
  • Revoking an API key or deleting a workspace stops new tool calls within 30 seconds.
  • POST /v1/workspaces/open returns a tool token with a ready workspace (see Open a workspace).

Requests and responses

  • Bodies are JSON: send Content-Type: application/json only with a body. Unknown properties are rejected with 422 validation_failed.
  • IDs are UUIDv7 strings in lowercase canonical form. Your own names (workspace_key, template slugs) are separate fields.
  • Timestamps are RFC 3339 in UTC with Z. Units are in field names: cpu_millis (1000 = one vCPU), memory_mib, disk_gib, cpu_seconds, egress_bytes.
  • Every response carries X-Request-Id. Send your own X-Request-Id (8-64 characters of A-Z a-z 0-9 . _ -) to correlate requests; otherwise the server generates one. Errors carry it as request_id.
  • Errors use one envelope, described in Errors:
JSON
{
  "error": {
    "code": "conflict",
    "message": "The workspace has a suspend operation in progress; retry when it completes.",
    "request_id": "req-9b0e1d2c3f4a5b6c7d8e9f00",
    "retryable": true,
    "operation_id": "01a0e5a8-3ef0-7ecb-975e-dff2d5ca6e33",
    "details": {
      "reason": "operation_in_progress",
      "active_operation_id": "01a0e5a8-3ef0-7ecb-975e-dff2d5ca6e33",
      "active_operation_kind": "suspend"
    }
  }
}

Idempotency

Send an Idempotency-Key header (1-255 visible ASCII characters) on requests that create something or start an operation, so a retry after a network failure cannot do it twice.

  • The server stores the response per principal, route and key for 24 hours. A retry with the same key and the same body returns the original status and body.
  • The same key with a different body is 422 idempotency_mismatch.
  • A duplicate that arrives while the first request is still running waits and then replays, or gets a retryable 409 conflict. Retry the same request with the same key.
  • Generate one key per intent (for example a UUID per user action) and reuse it for every retry of that intent.

Routes that accept it include POST /v1/workspaces/open, POST /v1/workspaces/{id}/suspend, .../suspend-when-idle, .../resume, .../snapshot, .../fork, .../close, .../reset, .../save-as-template, PATCH /v1/workspaces/{id}/caps, DELETE /v1/workspaces/{id}, secret creation and rotation, template builds, draft create, discard, states, test instances and publish, and volume create, delete, attach and detach. On the cell gateway, PUT .../files and POST .../files/patch accept it too (up to 100 characters of A-Z a-z 0-9 . _ : -); an execution of a file-first workspace is idempotent by its execution_id instead.

Pagination

Lists take ?limit= and ?cursor= and answer:

JSON
{ "data": [], "next_cursor": null }

Pass next_cursor as cursor to get the next page; null means the last page. Most lists take limit from 1 to 200 (default 50); file trees, diffs and workspace changes take up to 1000. Cursors are opaque.

Operations

Lifecycle calls (open, suspend, resume, snapshot, fork, delete, close, reset) create an operation and answer 202:

JSON
{ "operation": { "id": "01a0e5a8-3ef0-7ecb-975e-dff2d5ca6e33", "kind": "suspend", "state": "queued", "workspace_id": "01a0e5a8-3edd-74ba-b489-d62b8925e342", "created_at": "2026-09-28T12:00:00Z", "updated_at": "2026-09-28T12:00:00Z" } }
Field Meaning
kind open, suspend, resume, fork, snapshot, restore, delete, reset, resize, layer_snapshot, volume operations, ...
state queued, capacity_pending, running, succeeded, failed or canceled. The last three are terminal.
state_reason Why it is in that state, for example template_downloading while running.
created_at, started_at, completed_at When it was created, first entered running, and finished.
error On failure: {code, message, retryable, details}.
result On success: what the operation produced (for example a checkpoint id).

Poll GET /v1/operations/{operation_id} until the state is terminal. Operations stay readable after their workspace is deleted. GET /v1/workspaces/{id}/operations lists a workspace's operations, newest first.

POST /v1/workspaces/{id}/suspend-when-idle with {"after_seconds": 60} (0 to 3600; 0 = as soon as it is idle) creates no operation: it records a suspend when idle and answers 202 with {workspace, operation: null, suspend_request: {requested_at, after_seconds, not_before}}. The suspend, when the workspace has been idle that long, is a suspend operation of its own. If a suspend is already in progress, the answer is {workspace, operation: <that suspend>, suspend_request: null} and nothing is recorded. DELETE on the same path cancels a pending request (200 with the workspace, idempotent). The workspace view shows it as idle.suspend_request.

GET /v1/operations/{operation_id}/progress reads a suspend's live progress without waking the workspace: {progress: {operation_id, state, workspace_state, phase, cancelable, supported, safe_action, phase_started_at, deadline_at}}; GET /v1/operations/{operation_id} of a suspend carries the same progress. POST /v1/operations/{operation_id}/cancel (with an Idempotency-Key) cancels a suspend before its capture starts, while progress.cancelable is true. It answers 202 with {operation, progress} (progress.accepted: true) while the workspace is put back as it was, and 200 once the operation is canceled with error.code: suspend_canceled; poll the operation until then. Once capture has started it answers 409 conflict suspend_capture_started: wait for the operation, then resume (errors).

Bounded waits

Instead of polling in a loop, send Prefer: wait=<seconds> (at most 20):

Shell
curl -sS "https://api.shardflux.dev/v1/operations/$OPERATION_ID" \
  -H "Authorization: Bearer $SHARDFLUX_API_KEY" \
  -H "Prefer: wait=20"

While the operation is not terminal, the server holds the response until its state or state_reason changes, the wait elapses, or you disconnect. The response is always the normal representation, with Preference-Applied: wait=<seconds>. Without Preference-Applied the server did not wait: poll with backoff. Template builds (GET .../template-builds/{build_id}) take the same header.

Start deadlines

A queued open, resume or fork (capacity_pending) carries error.details.deadline_at, 15 minutes after the operation was created, and error.details.queue_position, its place in line (starts are served in the order they were requested). While new capacity is starting for it (error.details.pool.scale_out: requested), the deadline moves to 5 minutes after pool.expected_ready_by, at most an hour after creation. While the workspace's files are being moved to new storage (state_reason: preserving_files), the deadline is an hour after creation. Past the deadline, or after 2 minutes when no more capacity can be added right now, the operation fails with error.code: capacity_unavailable and retryable: true (details.retry_after_seconds in the second case, and at once when the start queue is full): nothing was started, and a suspended workspace stays suspended. Send the same request again.

Open a workspace

POST /v1/workspaces/open creates the workspace on first use and reconnects to or resumes it afterwards. It never resets an existing workspace.

Shell
curl -sS -X POST https://api.shardflux.dev/v1/workspaces/open \
  -H "Authorization: Bearer $SHARDFLUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=20" \
  -d '{"key": "customer-42/main", "template": "default"}'
Status Meaning
200 The workspace is running and ready. cell_endpoint and tool_token are set.
202 Poll operation (see above), then request a tool token.

The body is {workspace, operation, cell_endpoint, tool_token, created}. created is true when this request created the workspace (a new key, or a key whose session ended) and false when it reconnected to or resumed one. With Prefer: wait, the open is held until its operation is terminal or the wait elapses. Concurrent opens of one key share one workspace and one operation, and exactly one of them answers created: true.

Body fields: key (required, 1-200 characters without control characters), template (required, a slug; default is the platform default template, python-node-browser), caps (cpu_millis, memory_mib (the most memory it may use), disk_gib and memory_mib_held (what it holds while idle); see elastic memory; allocation_mode is deprecated, accepted and ignored), agent_label, tools, secrets (up to 50 names), inputs ({NAME: value}), lifetime (persistent or session), mode (processful, the default for a new key, or file_first; see File-first workspaces), like and volumes (below). The workspace view reports mode and tree_revision (null for a processful workspace), and memory: allocation_mode (elastic, or fixed for a workspace that holds its whole maximum), promised_mib (the most it may use), held_mib (what it holds while idle) and plugged_mib (what it holds above its floor while it runs, else null). See Workspaces and Lifecycle.

like names another workspace's key within the selected project and applies only to a new key: the new workspace gets a fresh disk with that workspace's exact template version, sizes, secret bindings, inputs and volumes, and the fields you send override what it copies. An existing key keeps its disk and template. volumes maps guest mount paths to project volume names or {name, mode} (ro or rw): a supplied set replaces the workspace's mounts and omission preserves them. Existing-key replacements advance one operation per open; waited SDKs continue under one budget. See Volumes.

Resume a workspace

POST /v1/workspaces/{id}/resume creates a resume operation (or joins the resume or open already running) and answers 202 with it. Send Prefer: wait=<seconds> (at most 20) and an optional body {"agent_label": ..., "tools": [...]} to have it held like an open:

Shell
curl -sS -X POST "https://api.shardflux.dev/v1/workspaces/$WORKSPACE_ID/resume" \
  -H "Authorization: Bearer $SHARDFLUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=20" \
  -d '{"agent_label": "my-agent", "tools": ["exec", "files"]}'
Status Meaning
200 The workspace runs: {workspace, operation, cell_endpoint, tool_token}, with Preference-Applied: wait=<seconds>. The token is for agent_label and tools, so the next tool call needs no token request. A workspace that was already running answers 200 at once with operation: null and a token.
202 Not finished within the wait, or the resume failed: {operation, workspace}. Poll the operation, then request a token.

The succeeded operation's result says how the workspace came back: resume_path (local_cache, prestaged or download: memory restored from the checkpoint; thaw: resumed while its suspend was still being written, the same VM continued in place; cold_boot: the saved disk was booted after a platform runtime change, or the workspace's disk after its host failed; reset_blank_layer: the first start after a reset), memory_restored (true or false) and, for a cold boot, cold_boot_reason (runtime_changed or host_lost). A workspace restored after its host failed also has host_lost (detected_at, restored_from: disk or checkpoint, and for a checkpoint restored_checkpoint_id and state_as_of). See When a resume boots instead and If a workspace's host fails.

Without the preference, a workspace that is already running is 409 conflict (already_running). tools beyond the API key's tool permissions are 403 forbidden before anything is created. The SDKs (@shardflux/sdk 0.9.0+, shardflux 0.5.0+), the CLI (0.5.0+) and the MCP server (0.4.0+) wake a suspended workspace with this one request.

Fork a workspace

POST /v1/workspaces/{id}/fork with a body {"key": ...} creates a fork operation and answers 202 with it. Send Prefer: wait=<seconds> (at most 20) and optionally agent_label and tools to have it held until the copy runs:

Shell
curl -sS -X POST "https://api.shardflux.dev/v1/workspaces/$WORKSPACE_ID/fork" \
  -H "Authorization: Bearer $SHARDFLUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=20" \
  -d '{"key": "customer-42/experiment", "agent_label": "my-agent", "tools": ["exec", "files"]}'
Status Meaning
200 The copy runs: {operation, workspace, source_workspace_id, cell_endpoint, tool_token}, with Preference-Applied: wait=<seconds>. workspace is the copy, and the token is the copy's, for agent_label and tools, so the next tool call needs no token request.
202 Not finished within the wait, or the fork failed: {operation, workspace}. Poll the operation, then request a token for the copy.

tools beyond the API key's tool permissions are 403 forbidden before the copy is created.

The copy keeps the source's secret bindings, text inputs and egress policy override. The override becomes version 1 of the copy's own history (GET /v1/workspaces/{id}/egress-policy on the copy), so the copy starts under the same outbound rules; later changes to either workspace's override apply to that workspace only.

Resize a workspace

PATCH /v1/workspaces/{id}/caps changes the memory, CPU or disk of any workspace, running or suspended, without a restart. The body has any of memory_mib (the maximum), memory_mib_held (the floor), cpu_millis and disk_gib, at least one; allocation_mode is deprecated, accepted and ignored. Each replaces the stored value, so every later start uses it. Send an Idempotency-Key, and Prefer: wait=<seconds> (default 20, at most 55) to have the answer held until the resize has finished:

Shell
curl -sS -X PATCH "https://api.shardflux.dev/v1/workspaces/$WORKSPACE_ID/caps" \
  -H "Authorization: Bearer $SHARDFLUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=20" \
  -d '{"memory_mib": 6144}'
Status Meaning
200 The resize finished: the result below, with the workspace's caps.
202 Not finished within the wait: {operation, workspace}. Poll the operation; its result is the same body.
JSON
{
  "workspace_id": "01a0e5a8-3edd-74ba-b489-d62b8925e342",
  "operation_id": "01a0f2c4-5d10-7a3e-9b21-6c4f8e2a1d07",
  "state": "running",
  "memory": {"applies_at": "now", "allocation_mode": "elastic", "requested_mib": 6144, "target_mib": 6144,
             "previous_mib": 4096, "applied_mib": 6144, "held_mib": 1024, "converged": true,
             "at_maximum": false, "limit_reason": null, "reason": null},
  "caps": {"cpu_millis": null, "memory_mib": 6144, "disk_gib": null, "allocation_mode": "elastic", "memory_mib_held": 1024}
}

The result has memory, cpu and disk for the resources the request named (memory_mib_held reports under memory):

Field Meaning
applies_at now: applied live, with every process and its memory kept. resume: the workspace is suspended and resumes at the new size, before its first call. next_start: applies the next time the workspace boots; nothing restarts now.
requested_*, target_*, previous_*, applied_* What the request asked for, what the workspace gets within its bounds, what it had, and what is live after the call (null when it is not running).
at_maximum true when the target is the most the workspace can have: the smaller of the plan's and the template's maximum. With previous_* equal to target_*, the resize changed nothing: a workspace already uses its full CPU and memory from boot and its disk grows on its own as it fills.
limit_reason plan or template: lowered to that maximum, as caps at an open. host_capacity: part of a grow was added; resize again for the rest. shrink_stalled: the processes use the memory, so the workspace keeps it. Otherwise null.
reason Why it does not apply now: suspended, stopped, boot_vcpus (more vCPUs than the workspace started with), region (more memory than it can add while it runs), below_base (less than the 1 GiB it boots with), legacy_layout (use upgrade to enable live memory resizing). Otherwise null.
allocation_mode, held_mib, converged Memory only: elastic (or fixed for a workspace that holds its whole maximum), the floor (null when fixed), and whether a live change has reached the workspace.

The SDKs wait for a 202 themselves: workspace.resize() (TypeScript 0.14.0+), ws.resize() (Python 0.10.0+). Refusals are in Errors: shrink_not_supported, exceeds_memory_mib, not_supported_for_mode and workspace_deleted.

The resize body accepts at_least: true. Each named numeric resource at or below its current configured size is unchanged; a smaller disk is a no-op, so memory in the same request can grow. A larger value uses the usual bounds. The response reports each named resource with its requested, previous, target and applied fields.

Upgrade a workspace

POST /v1/workspaces/{id}/upgrade accepts { "at": "now" } or { "at": "next_resume" } and returns an operation. The cold start keeps the workspace disk and ends memory and processes. A scheduled operation reports applied: next_resume; the applying start reports applied: now, cold_boot_reason: upgraded, memory_restored: false, and the applied upgrade target. A resume that continues the workspace in place (resume_path: thaw) keeps a scheduled target pending. Workspace views expose upgrade_available and upgrade_pending. Supports Idempotency-Key. See lifecycle.

Inbound ports

Serve a TCP port of a processful workspace at https://<port>-<handle>.shardflux.app (guide). The routes need the workspace's lifecycle permission, like labels:

Route Answer
PUT /v1/workspaces/{id}/ports/{port} 201 exposed now, 200 already exposed (unchanged): {port, url, created_at, callback}
GET /v1/workspaces/{id}/ports 200 {ports: [{port, url, created_at, callback}]}, ordered by port
DELETE /v1/workspaces/{id}/ports/{port} 204. Its tokens, links and callback URL stop working at once, also when the port is exposed again
POST /v1/workspaces/{id}/ports/{port}/tokens {ttl_seconds} 201 {token, expires_at, url}: ttl_seconds 60 to 86,400 (default 3,600)
POST /v1/workspaces/{id}/ports/{port}/links {ttl_seconds, path} 201 {url, expires_at}: ttl_seconds 60 to 604,800 (default 86,400); path starts with / (default /)
POST /v1/workspaces/{id}/ports/{port}/callback 201 {url, created_at}: creates the callback URL, or replaces it. The URL is returned only here
DELETE /v1/workspaces/{id}/ports/{port}/callback 204

callback is {created_at} while the port has a callback URL, else null. A request to a port URL carries one of:

  • Authorization: Bearer sfp_... (a port token), or X-Shardflux-Token: sfp_... when the app reads Authorization itself;
  • the cookie a signed link sets (open the link's url in a browser: it sets the cookie for that host and redirects to path);
  • the callback URL's path: a request to <callback url><rest> reaches /<rest>, query kept.

The app receives the public Host, X-Forwarded-For, X-Forwarded-Proto: https and X-Forwarded-Host, never the platform's credential. A request that woke the workspace carries Server-Timing: sf-wake;dur=<ms>;desc="<frozen | hibernated | suspended>". Refusals: errors; limits: limits.

Rate limits

A request over a limit is 429 rate_limited with retryable: true and a Retry-After header in seconds (also in details.retry_after_seconds when the API sets it). Wait that long before retrying.

Limit Scope
Tool tokens (including the token of a ready open): 3600 per hour. Past it, an open still succeeds with tool_token: null; get a token later from POST /v1/workspaces/{id}/tool-tokens per API key
Template package lookups: 120 per minute per organization
Cell gateway: a workspace at a resource limit answers 429 with Retry-After: 1 per workspace

A 429 can also come from the network edge, with Retry-After: 60 and request_id: "edge". Plan limits (sizes, allowances) are not rate limits: they answer 402 or 403. The plan's workspaces working at once answers 429 quota_exceeded, which is retryable. See Limits.

Cell endpoints

The cell gateway serves a running workspace's tools at its cell_endpoint. Every call carries Authorization: Bearer <tool token>; the token must name the workspace and include the tool the call needs.

Shell
CELL=$(jq -r .cell_endpoint token.json); TOKEN=$(jq -r .token token.json)
curl -sS -N -X POST "$CELL/v1/workspaces/$WORKSPACE_ID/exec" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Accept: application/x-ndjson" \
  -d '{"session_id": "build-1", "argv": ["bash", "-lc", "make test"]}'

One request starts the command and streams its output: with Accept: application/x-ndjson (or text/event-stream), POST /exec answers 200 with the same events as GET /exec/{session_id}/output?follow=true from offset 0, and X-Exec-Session-Id names the session. A dropped stream continues with GET /exec/{session_id}/output from the byte offsets already read; the command never runs twice. Without that Accept, the start answers with the session JSON and the output is read with GET /exec/{session_id}/output.

Method and path (under /v1/workspaces/{workspace_id}) Tool Does
POST /exec exec Starts a command (argv, no shell), and with Accept: application/x-ndjson or text/event-stream streams its output in the same response. resource_hint: on a workspace that adds memory as it grows (one started earlier), heavy gives it its memory before the command starts, light starts it at once, auto (the default) decides from the command; a workspace that sees its maximum from boot starts every command at once. Idempotent by session_id: 201 created, 200 it already existed and nothing ran again (a streamed start answers 200 and streams the existing session). cwd is an absolute path (default /home/user); a relative one is 422 validation_failed (invalid_cwd).
GET /exec/{session_id} exec Session status.
GET /exec/{session_id}/output exec Output from byte offsets (stdout_offset, stderr_offset) as NDJSON, or Server-Sent Events with Accept: text/event-stream. follow=true (default) keeps the stream open until exit.
GET /exec/{session_id}/attach exec WebSocket: output with offsets, signal and cancel ({"type": "cancel", "grace_ms": 10000}, as POST .../cancel).
POST /exec/{session_id}/signal exec Signals the session's process group.
POST /exec/{session_id}/cancel exec Stops the command and every process it started, background and setsid processes included: SIGTERM, then SIGKILL after grace_ms (0 to 60000; 0 or omitted is 5000). Answers once they have all exited, also for a command whose supervisor was lost (lost). A command that already exited keeps its result, and anything it left running is stopped. While a process cannot exit yet (uninterruptible I/O): 503 dependency_unavailable with Retry-After; cancel again. Workspaces on python-node-browser 13, ubuntu-24.04 4, ubuntu-22.04 2, debian-12 2 and later template versions; earlier versions signal the command's process group.
POST /pty pty Opens a terminal session (idempotent by session_id).
GET /pty/{session_id}, DELETE /pty/{session_id} pty Status; close (SIGHUP, then SIGKILL after 2 seconds).
POST /pty/{session_id}/resize, POST /pty/{session_id}/input pty Resize; write input bytes.
GET /pty/{session_id}/attach pty WebSocket: input, output with offsets, resize. read_only=true watches without typing: input and resize are answered with an error message (forbidden, details.reason read_only_attach). passive=true never wakes or keeps the workspace awake while the terminal is idle.
GET /processes, POST /processes/{pid}/signal process Lists and signals guest processes.
GET /files?path= files Reads a file: offset and length, or a single Range: bytes=start-end. X-File-Size gives the size, X-File-Revision the SHA-256 of the whole file (files up to 16 MiB), and X-Served-From: disk a read of a sleeping workspace's disk.
PUT /files?path= files Writes a file from a raw application/octet-stream body: atomic replace, or append=true. mode, create_parents. Acknowledged after fsync (durable: true).
DELETE /files?path= files Removes a file or directory (recursive).
GET /files/stat, GET /files/list, POST /files/mkdir, POST /files/move files Stat (revision=true adds the content's SHA-256, regular files up to 256 MiB), list, create a directory, move or rename.
GET /computer computer The workspace desktop: {running, display, width, height, started_at, session, streams}. Never starts it.
POST /computer computer Starts the desktop (width 640-2560, height 480-1600; default 1280x800). A no-op while it runs.
DELETE /computer computer Stops the desktop; its windows close.
POST /computer/actions computer Runs actions (Claude's computer toolset members with their parameters) in order; screenshot, settle_ms, format, quality. Starts the desktop first if needed. Returns {results, screenshot?, cursor, display}.
GET /computer/frames computer WebSocket: the screen as images. Each change is a JSON {"type":"frame","seq","format","width","height","bytes","captured_at"} message followed by one binary message with the image; nothing while the screen is still; heartbeat every 20 s. format (jpeg, png), quality (1-100, default 70), max_fps (1-10, default 5). Starts the desktop first if needed; the workspace stays awake while it is open. Up to 4 streams per workspace (429 rate_limited, details.reason frame_streams_limit).
GET /computer/vnc computer WebSocket (subprotocol binary): the desktop as VNC, RFB 3.8 bytes in binary messages both ways, as noVNC's RFB client speaks it. View only; control=true takes over: the agents' input actions wait while it is open and answer the action error taken_over. Starts the desktop first if needed; the workspace stays awake while it is open. Up to 4 per workspace (429 rate_limited, details.reason vnc_streams_limit).
POST /computer/stream, DELETE /computer/stream computer Starts the viewer (interactive) and returns {port, path, interactive} to expose and link with the ports API; DELETE ends every view.
POST /files/search files Searches file contents under path: pattern (literal, or RE2 with regex), case_insensitive, include and exclude globs, max_matches, max_file_bytes, context_lines. Returns {matches, truncated, stop_reason, files_scanned}. Read-only.
POST /files/patch files Applies edits ([{old_text, new_text, replace_all}]) or a whole content atomically, optionally only at expected_revision (a SHA-256, or absent). Returns {path, revision, previous_revision, bytes_written, durable, replacements, file}. Accepts Idempotency-Key.
GET /changes files A layered workspace's changes against its template (path_prefix, limit, cursor, hash, summary).
POST /git/clone, GET /git/status, POST /git/commit git Clone (HTTPS only), status, add and commit.
POST /browser/screenshot, POST /browser/content browser Navigates the workspace's headless Chromium; returns a PNG, or the rendered page as HTML or text.
POST /keepalive any Keeps the workspace from idle suspend for seconds, and resident (not parked). Not tool activity.
POST /wake-hint any A tool call is coming: a parked workspace starts waking. 202 {residency} (resident, frozen, hibernated, restoring); a suspended workspace is 409 workspace_not_running, and a wake past the plan's workspaces working at once is 429 quota_exceeded at once. Not tool activity.
GET /executions/{execution_id} exec File-first workspaces: an execution's result (202 while it runs). See File-first workspaces.
GET /idle any The idle policy with its current timeout_seconds and basis, recent activity, idle_since, a pending suspend-when-idle request (suspend_request, else null) and suspend_at, the projected automatic suspend: the earlier of the policy's time and the request's.

Sessions and offsets

Exec and terminal sessions have a session_id you choose (or the server generates). Starting an existing session never runs anything again. Output is kept in the workspace and addressed by byte offsets that only grow: after a dropped connection, reconnect with the last offsets you processed instead of starting a new command. Output events have type (output, exit, heartbeat every 15 seconds, error), stream, offset and data (base64). Session ids starting with sfstart. belong to the template's start commands: you can read them but not start one.

On a workspace that adds memory as it grows (one started earlier), a session whose start grew the workspace (a heavy command, or resource_hint: "heavy") has memory_grow: {outcome, from_mib, want_mib, got_mib, deliver_ms}, with outcome delivered, partial (less than it asked for), missed (nothing could be added) or failed. A session started on a new fork before the fork's memory caps take effect waits for them and reports them the same way. The command starts in every case.

A start's timeout_ms (up to 604800000, a week) ends the command with SIGTERM, then SIGKILL after kill_grace_ms (0 to 600000; 0 or omitted is 5000). A cancel stops it now: SIGTERM, then SIGKILL after its grace_ms (0 to 60000; 0 or omitted is 5000), and the answer comes once the command has ended.

A command that cannot start (a cwd that is not a directory, a program that is not on PATH, an unknown user) is not an HTTP error: the start answers 201 with state: "failed_to_start" and error naming the cause, for example working directory "/home/user/app" is not a directory. Nothing ran, so the session has no exit code and no output; its output stream sends only the exit event.

Every exec and terminal runs with, lowest first: the guest's environment, the template's settings.env, the workspace's text inputs, then the request's env. Bound secrets and secret_refs are added on top; a secret whose name collides with any of those keys is 422 validation_failed (env_collision).

During lifecycle transitions

How the cell answers during transitions:

Situation Answer What to do
A lifecycle operation holds the workspace (suspend, resume, fork, snapshot, ...) 409 workspace_busy, retryable: true, operation_id, details.tool_gate: read_only during a snapshot capture (reads still work), else closed Wait for the operation, then retry.
The workspace is suspended 409 workspace_not_running, except reads served from its saved disk (file read, stat, list and search) Call POST /v1/workspaces/{id}/resume (with Prefer: wait, it returns a token) or open the key again, then retry with the new token.
The workspace is parked while idle Nothing: the call wakes it first, waiting while its host makes room when it has to. 503 service_unavailable (host_capacity) if it is still waiting when the call's wait ends (25 to 50 seconds, see limits), wake_failed if the wake failed. Retry after Retry-After.
The token's epoch is old 409 stale_epoch Get a new tool token and retry.

A refused call was never executed, so retrying it is safe. The SDKs, the CLI and the MCP server do this for you ("wake on use").

File-first workspaces

A workspace opened with "mode": "file_first" has no VM between commands (see File-first workspaces). Its cell endpoint answers the same files routes from the workspace's file tree, and runs each POST /exec as an execution:

Shell
curl -sS -X POST "$CELL/v1/workspaces/$WORKSPACE_ID/exec" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"execution_id": "build-42-attempt-1", "argv": ["bash", "-lc", "make -C /home/user/app"], "timeout_ms": 600000}'
  • execution_id is required (8-128 characters of A-Z a-z 0-9 . _ : -, starting with a letter or a digit); it is the idempotency key. output_limit_bytes (1 to 16,777,216, default 1 MiB) bounds stdout and stderr each. session_id is ignored.
  • The call answers when the command has ended: 201 when this call ran it, 200 with the recorded result for any other call with the same id (after waiting for a running one). The same id with a different request is 409 conflict (execution_id_reused).
  • The result: execution_id, state (succeeded, failed, lost), base_revision, tree_revision, exit_code, term_signal, timed_out, stdout and stderr (base64) with stdout_truncated and stderr_truncated, changed ([{path, change, type}], up to 10,000, changed_truncated), timings, error, created_at, finished_at.
  • GET /executions/{execution_id} returns the result: 200 once it ended, 202 while it is queued or running, 404 for an unknown id. Results are kept for 7 days.
  • While an execution runs, a second one and every file change are 409 workspace_busy (execution_in_progress, details.execution_id). 503 service_unavailable (no_execution_host, with Retry-After): retry with the same id.
  • Every files response carries X-Tree-Revision. PUT and DELETE /files, files/mkdir, files/move and files/patch take If-Match: <tree revision>: another current revision is 409 conflict (tree_revision_mismatch, details.current_tree_revision) and nothing changes. Idempotency-Key replays answer with Idempotent-Replayed: true.
  • Only paths under /home/user exist (outside_tree_root). Exec sessions, terminals, processes, git, browser, changes, keepalive and idle are 409 conflict (not_supported_for_mode), and wake-hint answers resident.

WebSocket close codes

Exec and terminal attach streams close with 1000 after the exit event and 1001 when the gateway shuts down (reconnect with your offsets). Otherwise the code is 4000 plus the HTTP status of the error: 4401 unauthenticated, 4403 forbidden, 4409 stale_epoch, workspace_busy, workspace_not_running or conflict, 4429 rate_limited or quota_exceeded (workspaces working at once), 4500, 4503 and 4504; 4410 means the workspace is gone. Before closing, the gateway sends an error message with the full error envelope; the close reason is compact JSON with the error's code and retryable (and operation_id, request_id when they fit). The SDKs read both, so a 4429 keeps its own code (quota_exceeded or rate_limited) and a 4503 its details, such as reason: host_capacity (TypeScript 0.22.0+, Python 0.18.0+).

Idle deletion retention

POST /v1/workspaces/open accepts retention: {"delete_after_idle_days": 14} for persistent workspaces. PUT /v1/workspaces/{id}/retention takes that policy object or JSON null to follow the project default and returns the workspace. GET and PUT /v1/projects/{id}/retention read and set a default policy: {"delete_after_idle_days": 14, "labels": {"app": "chat"}}. All selector labels must match exactly; omit labels to match every persistent workspace. A workspace's own setting wins. JSON null clears the project default. Use a project key for its own project or an owner, admin or member session.

The workspace view includes retention: {"delete_after_idle_days": 14, "delete_at": "2026-10-18T12:00:00Z"}, or null when no policy applies. Days are integers from 1 through 3650. Calls and ongoing work postpone deletion; the projected time moves with activity. The audit event for an automatic delete has reason: retention.

Volumes

Client support: TypeScript SDK 0.18.0+, Python 0.14.0+, CLI 0.13.2+, MCP 0.12.0+.

POST /v1/projects/{id}/volumes/open with {name, quota_gib?} reuses the project's volume of that name or creates it (default 1 GiB). Mount volumes on a workspace with volumes on open; access follows the mount mode. Shared volumes are outside workspace snapshots and forks. See limits.

Website sign-ins

Site logins keep website credentials in an encrypted store. A person's session manages their items; an organization owner or admin can also manage project-scoped items. A project API key with browser permission can list and add logins in its own project. Reveal requires a person's session. Every reveal and store fill is audited.

Method and route Request or result
GET /v1/site-logins {data: [login]} metadata across the person's eligible projects; filters organization_id, project_id, site, created_by=agent and created_after (ISO time). Results are ordered by creation time.
POST /v1/site-logins {site, username?, password?, totp?, passkeys?, related_origins?, scope?, organization_id?, project_id?, conversation_id?, created_by?} → login metadata, 201.
PATCH /v1/site-logins/{id} Update scope, its project or thread id, username, site, related origins or values → login metadata. Value changes retain the previous encrypted version.
DELETE /v1/site-logins/{id} Revoke the login → 204.
POST /v1/site-logins/{id}/reveal {} → {password?, totp_seed?, totp_code?, totp?, passkey_count} on the person's device. Private keys are excluded from reveal.
POST /v1/site-logins/{id}/vault-link {vault, item} → metadata with local_vault: {vault, item, synced_at}. Vault is hsec, 1password, bitwarden or keychain.

scope defaults to agents: every agent the person runs in that organization. project selects project_id; conversation selects conversation_id. organization_id is inferred from a selected project or thread, or the person's sole eligible organization; supply it when selecting among several. API keys add with scope: "project". A live login is identified by its organization, scope, scope id, site and username. Adding that identity again returns 409 conflict with details: {reason: "site_login_exists", login_id}; update it through PATCH instead.

site is an HTTPS origin; local development also accepts HTTP localhost. related_origins lists the other exact origins used by that site's sign-in flow. On creation they share the site's registrable domain (for example, mail.example.co.uk and accounts.example.co.uk). The login owner, or an organization owner/admin for a project login, can approve other origins through an audited PATCH. username is metadata and may be empty.

totp is {seed, digits?, period?, algorithm?}: base32 seed, digits (default 6), period in seconds (default 30), and SHA1 (default), SHA256 or SHA512. passkeys contains {credential_id, rp_id, user_handle, private_key, sign_count, created_at}; private_key is PKCS#8 DER in base64. Handle value fields through a secure credential flow. Lists return has_password, has_totp and passkey_count with metadata; a reveal additionally returns the current TOTP code and its setup parameters. Reads of metadata do not return credential values.

For example, a stored login with an authenticator and a passkey has this metadata shape (selected fields):

JSON
{
  "id": "01a166ab-6c8d-71ac-b3b8-e70802a020d8",
  "site": "https://login.example.com",
  "username": "test@example.com",
  "scope": "agents",
  "has_password": true,
  "has_totp": true,
  "passkey_count": 1,
  "version": 1,
  "created_by": {
    "kind": "person"
  }
}

The full metadata view also carries organization_id, owner_user_id, scope_id, related_origins, local_vault, created_at, updated_at and last_used_at. created_by.kind is person, import or agent; agent provenance can name its thread, workspace and task. local_vault and last_used_at are null until a copy or use is recorded. See website sign-in limits for field sizes, item counts and request timing.

Fill a website sign-in

POST /v1/credential-requests accepts {workspace_id, site, fields, public_key, account?, format?}. account selects a saved username. fields selects username, password, totp or passkey; request format: 2 for passkeys and structured authenticator settings. public_key is the requester's base64 X25519 public key. A workspace's credential relay authenticates with its bearer token and accesses only that workspace's visible logins.

JSON
{
  "workspace_id": "01a166ab-6fcd-76c4-88e2-ab4e1ef8eb28",
  "site": "https://board.example.com",
  "account": "board@example.com",
  "fields": ["password", "totp"],
  "public_key": "<requester's base64 X25519 public key>",
  "format": 2
}

Lookup checks the thread's scope, its project's scope and its starter's agents scope. Within a scope, account selects the username; omitting it selects the most recently used login that has the requested fields. Exact site or related-origin matches are required. A match returns state: "fulfilled" at once; its metadata response includes id, workspace_id, project_id, site, account, fields, public_key, created_at, expires_at and finished_at. GET /v1/credential-requests/{id} returns {request, ciphertext}; only that workspace's relay receives the ciphertext. Only requested values are sealed. Format 2 plaintext is {v:2, login_id, username?, password?, totp_seed?, totp?, passkeys?}. The sealed envelope remains compatible with the original format; omitting format keeps the original plaintext shape.

A new request carries the request lifetime in expires_at. A waiting sign-in can be answered within that lifetime after the agent's synchronous call returns: saving the answer makes the next sign-in use the store and sends the thread SIGN-IN SAVED: <site> (<username>). Call sign_in or credential_fill again. Expired requests leave the sign-in list. Fulfilling an expired request returns 409 conflict with details.reason: credential_request_expired; a request whose relay lost authorization returns 409 conflict with details.reason: credential_relay_revoked.

GET /v1/credential-requests returns {data, next_cursor}. With a person's session, omit project_id for pending requests across the projects they work in; an API key stays in its project. GET /v1/credential-requests/events returns credential events, including store changes, reveals and fills; personal events are visible to their owner.

Answer a website sign-in request

Your product can present a secure credential form on any device. Read the request with GET /v1/credential-requests/{id} and send the supplied values directly to your secure handler, outside agent context. The handler can save the login in the chosen scope and seal the requested fields to the requester's public key (TypeScript SDK 0.32.5+):

TypeScript
import { sealCredential } from '@shardflux/sdk';

const { request } = await account.credentialRequests.get(requestId);
await account.siteLogins.add({
  organizationId,
  site: request.site,
  username: credential.username,
  password: credential.password,
  ...(credential.totp_seed ? { totp: { seed: credential.totp_seed } } : {}),
  scope: 'agents',
});
const ciphertext = sealCredential(request.public_key, request, credential);
await account.credentialRequests.fulfil(request.id, ciphertext);

Here credential contains only the requested username, password and totp_seed fields supplied through the secure form. POST /v1/credential-requests/{id}/fulfil takes {ciphertext} and returns the updated request metadata with state: "fulfilled". The ciphertext is a base64 sealed envelope bound to that request id and site. Saving the login before fulfilling the request lets another computer or the agent's next call use the store. A one-time fill sends the sealed answer without adding a login. POST /v1/credential-requests/{id}/deny with {} declines the request.

The workspace relay reads the encrypted answer and reports the fill with POST /v1/credential-requests/{id}/result and {filled: true|false}; reporting clears the ciphertext. The compatibility route PUT /v1/site-credentials with {workspace_id, site, credential, scope?} also remembers an answer in the central store. Its scopes are conversation, project (default), and machine (the person's agents scope).

Store a login created by an agent

A workspace relay calls POST /v1/site-logins/agent with {workspace_id, site, related_origins?, username?, password?, password_secret?, totp?, passkey?, event, login_id?}. Events are created, saved, password_changed, totp_enrolled, passkey_created and passkey_used. saved stores an existing account's login: it takes password_secret, the name of an agent credential in that computer's environment, instead of password, and the server copies that credential's value into the login. Without such a credential it returns 404 not_found with reason credential_not_found. The response is login metadata. A new item belongs to the starter's agents scope; login_id can select another item visible to the thread. Existing account names and related origins stay as saved; a person changes them through PATCH. Changing them through the relay returns 403 forbidden. The owning person must be a current non-billing organization member for agents-scope fills and stores; this also applies to an API key's creator. The previous encrypted value is retained on a value change. Agent tools send these values directly from the computer, and return metadata to the model.

GET /v1/site-logins?workspace_id={id} with that relay returns visible login metadata. A person's device can discover new items with created_by=agent&created_after={time}, copy them to a local vault and record the copy with vault-link. The TypeScript (0.32.5+) and Python (0.28.3+) references cover the store methods; Website sign-ins shows the agent flow.

Browser sessions

GET /v1/browser-profiles returns metadata. PUT /v1/browser-profiles/{name} takes {scope:"project"|{workspace_id}, sites:[{site,storage_state}]}; DELETE /v1/browser-profiles/{name}?site=… revokes a selected site (omit site to remove the profile). State uses Playwright cookies and origins/localStorage; workspace scope takes precedence over project scope.

Stored encrypted with AWS KMS; only this project's workspaces can load it, and every load is logged.

Session replay status

Exec and PTY session responses include optional tunnel_replay when the guest reports it: enabled retains tunnel bytes for reconnection, disabled opens the tunnel without replay, and disabled_memory_budget opens it without replay while the guest replay budget is in use. The field is absent when the guest does not report replay status. A tunnel without replay can be connected again after a lifecycle interruption.

View this page as Markdown