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.5.0", "minimum_supported": "0.1.0", "upgrade_command": "npm install -g @shardflux/cli@latest", "release_notes_url": "https://www.npmjs.com/package/@shardflux/cli?activeTab=versions" }] }

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 that is not distributed yet.

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 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. 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; in a rare race it 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, 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, 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 no_ready_host while capacity_pending, or 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} (30 to 3600) 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.

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.

Starts that wait for capacity

An open, resume or fork that no host can admit yet is capacity_pending, with error.details.deadline_at set to its deadline, 15 minutes after the operation was created. A start still pending then fails with error.code: capacity_unavailable and retryable: true: nothing was started, and a suspended workspace stays suspended. Send the same request again later.

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": "python-node-browser"}'
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}. 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.

Body fields: key (required, 1-200 characters without control characters), template (required, a slug), caps (cpu_millis, memory_mib, disk_gib), agent_label, tools, secrets (up to 50 names), inputs ({NAME: value}), lifetime (persistent or session) and mode (processful, the default for a new key, or file_first; see File-first workspaces). The workspace view reports mode and tree_revision (null for a processful workspace). See Workspaces and Lifecycle.

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.

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.

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

An edge rate rule can also answer 429 with Retry-After: 60 and request_id: "edge". Plan limits (concurrent workspaces, sizes, allowances) are not rate limits: they answer 402 or 403. 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 -X POST "$CELL/v1/workspaces/$WORKSPACE_ID/exec" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"session_id": "build-1", "argv": ["bash", "-lc", "make test"]}'
curl -sS "$CELL/v1/workspaces/$WORKSPACE_ID/exec/build-1/output?follow=true" -H "Authorization: Bearer $TOKEN"
Method and path (under /v1/workspaces/{workspace_id}) Tool Does
POST /exec exec Starts a command (argv, no shell). Idempotent by session_id: 201 created, 200 it already existed and nothing ran again. 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.
POST /exec/{session_id}/signal exec Signals the session's process group.
POST /exec/{session_id}/cancel exec SIGTERM to the process group, SIGKILL after a grace.
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 a grace).
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.
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.
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. 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.

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

The cell does not resume a workspace by itself:

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 of its disk (file read, stat, list and search) while a host still holds it 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. 503 service_unavailable with host_capacity or wake_failed when it cannot be woken right now. 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) means no host had room: 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, 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 code.

View this page as Markdown