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:
{ "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:
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,gitandbrowser, which bound what its tool tokens may do.GET /v1/mereturns the key's organization, project andtool_permissions. - An API key is a server credential. Keep it out of browsers and client-side code.
- A missing or non-Bearer
Authorizationheader, and a malformed, unknown, revoked or expired key, are401 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.
curl -sS https://api.shardflux.dev/v1/auth/session -H "Authorization: Bearer $SHARDFLUX_SESSION_TOKEN"- Signing in.
/v1/authhas the console's sign-in routes:register,verify-email(andverify-email/resend),login,mfa/challenge,session,logout,logout-all,sessions,step-up,password/change,password/reset/request,password/reset/confirm,email/change,email/change/confirmand themfa/totpandmfa/recovery-codesroutes. Where the console gets a cookie, the answer carriessession_token(sfu_...) andsession_expires_at. Emailed links are the console's (https://app.shardflux.dev/auth/verify-email#token=...): post thetokenof 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_tokenand 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/logoutends it. - What it can do. Every
/v1route takes it as the person: their memberships and roles decide, and routes that need a project takeproject_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 gets403 forbiddenthere. - Refusals. An unknown, revoked or expired session is
401 unauthenticated. A session waiting for its second factor gets403 mfa_required, an unverified email403 email_unverified, and a sensitive action without a recent step-up403 step_up_required. See Errors. - A project API key is not a session: the
/v1/authroutes that need a session answer it with401.
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.
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, withX-Served-From: disk) and every other call with409 workspace_not_running. While a suspend or a requested resume is in progress, the request is409 conflictwithdetails.reason: workspace_not_runningand the operation; a deleted workspace is409 conflictworkspace_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/openreturns a tool token with a ready workspace (see Open a workspace).
Requests and responses
- Bodies are JSON: send
Content-Type: application/jsononly with a body. Unknown properties are rejected with422 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 ownX-Request-Id(8-64 characters ofA-Z a-z 0-9 . _ -) to correlate requests; otherwise the server generates one. Errors carry it asrequest_id. - Errors use one envelope, described in Errors:
{
"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:
{ "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:
{ "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):
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.
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:
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.
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:
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_idis required (8-128 characters ofA-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_idis ignored.- The call answers when the command has ended:
201when this call ran it,200with the recorded result for any other call with the same id (after waiting for a running one). The same id with a different request is409 conflict(execution_id_reused). - The result:
execution_id,state(succeeded,failed,lost),base_revision,tree_revision,exit_code,term_signal,timed_out,stdoutandstderr(base64) withstdout_truncatedandstderr_truncated,changed([{path, change, type}], up to 10,000,changed_truncated),timings,error,created_at,finished_at. GET /executions/{execution_id}returns the result:200once it ended,202while it is queued or running,404for 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, withRetry-After) means no host had room: retry with the same id. - Every files response carries
X-Tree-Revision.PUTandDELETE /files,files/mkdir,files/moveandfiles/patchtakeIf-Match: <tree revision>: another current revision is409 conflict(tree_revision_mismatch,details.current_tree_revision) and nothing changes.Idempotency-Keyreplays answer withIdempotent-Replayed: true. - Only paths under
/home/userexist (outside_tree_root). Exec sessions, terminals, processes, git, browser, changes, keepalive and idle are409 conflict(not_supported_for_mode), andwake-hintanswersresident.
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.