Basics
Authentication
Send an API key or a session token as a bearer token:
Authorization: Bearer mk_…- API keys start with
mk_. Create and revoke them on the dashboard's API keys page. They do not expire. - Session tokens start with
ms_.mat logingets one; it lasts 7 days. - A few routes need a signed-in person and refuse API keys with 403: managing API keys, billing checkout and portal, and signing out.
Requests
- Unknown JSON fields are refused with 400, so a typo never passes silently.
- Times are RFC 3339 in UTC. IDs have a prefix:
sb-sandboxes,ex-commands,snap-snapshots,key-API keys,acct-accounts. - Everything you name is scoped to your account. Another account's sandbox is not found, as if it did not exist.
- Send an
Idempotency-Keyheader withPOST /v1/sandboxesto retry a create safely.
Errors
Every error has the same body. The message is written for people and safe to show them.
{ "error": { "code": "quota_exceeded", "message": "this account may run at most 10 sandboxes at once; pause or delete one" }}See Limits and errors for every code.
Streaming output
GET /v1/sandboxes/{id}/execs/{exec_id}/events streams newline-delimited JSON. Output arrives as base64 in data, because it is arbitrary bytes. offset counts all output bytes before the chunk, across both streams. To resume after a dropped connection, reconnect with ?after= set to the offset of the last chunk plus its decoded length. A sandbox keeps the last 1 MiB of each command's output; if you ask for older bytes, a truncated event says where the kept output begins. The stream ends with exited, whose code is the exit code, or null with signal set when a signal ended it.
Connecting a shell or browser
- Call
POST /v1/sandboxes/{id}/connectwith{"kind": "shell"}or{"kind": "browser"}, authenticated as usual. - Open the
wss://URL it returns within 60 seconds. The token in it works once. For a shell, append?cols=N&rows=N. - Shell: send keystrokes and receive output as binary messages; send resizes and receive the exit as JSON text messages. Browser: speak the Chrome DevTools Protocol.
Sandboxes
POST/v1/sandboxes
Create a sandbox. Returns once it is running. An empty body means all defaults.
- Auth
- API key or session
- Success
201 Created
Headers
- Idempotency-Key
- 1 to 128 visible ASCII characters. Retrying with the same key returns the original sandbox instead of creating another.
{ "size": "small", "keep_awake": false, "idle_timeout_minutes": 10, "egress": { "mode": "restricted", "allow": ["example.com", "*.example.org"], "essentials": true }, "secrets": ["ANTHROPIC_API_KEY"]}{ "id": "sb-4f1c9e27a0b3d5c86e12", "size": "medium", "status": "running", "keep_awake": false, "idle_timeout_minutes": 10, "egress": { "mode": "open" }, "egress_pending": false, "secrets": [], "created_at": "2026-10-30T09:12:44.512731Z", "cold_booted_at": null, "forked_from": null}Errors
400 invalid_requestidle_timeout_minutes must be 1-1440400 invalid_requestno such secrets: NAME400 invalid_request"…" is not a domain or *.domain400 invalid_requestIdempotency-Key must be 1-128 visible ASCII characters429 quota_exceededtoo many sandboxes created this minute; retry in N seconds429 quota_exceededthis account has no sandbox quota yet; ask us to enable it429 quota_exceededthis account may have at most N sandboxes429 quota_exceededthis account may use at most N MiB of sandbox memory429 quota_exceededthis account may run at most N sandboxes at once; pause or delete one503 unavailableno capacity available right now; retry shortly
- All fields are optional. size is small, medium (default) or large. egress.mode is open (default), restricted or none; essentials defaults to true. Up to 256 allowed domains and 64 secrets.
- When no host has room, the create may wait up to a minute for room before answering 503.
- Every attempt counts toward the 60-per-minute create limit, including retries with an Idempotency-Key.
GET/v1/sandboxes
List your sandboxes.
- Auth
- API key or session
- Success
200 OK
{ "sandboxes": [ { "id": "sb-4f1c9e27a0b3d5c86e12", "status": "running", … } ] }- Never wakes a sandbox.
GET/v1/sandboxes/{id}
Show one sandbox.
- Auth
- API key or session
- Success
200 OK
{ "id": "sb-4f1c9e27a0b3d5c86e12", "size": "medium", "status": "running", "keep_awake": false, "idle_timeout_minutes": 10, "egress": { "mode": "open" }, "egress_pending": false, "secrets": [], "created_at": "2026-10-30T09:12:44.512731Z", "cold_booted_at": null, "forked_from": null}Errors
404 not_foundno sandbox sb-…
- Never wakes a sandbox.
- status is creating, running, pausing, paused, resuming, stopped, deleting, deleted or failed. See Sandbox states in Concepts.
- cold_booted_at is when it last booted fresh from its files; processes from before then are gone. forked_from is the snapshot a fork was made from.
- held appears only on a sandbox held for review after suspected abuse, with the reason. Until it is cleared, anything that would run it (exec, files, shell, resume, restore, fork) fails with 403 forbidden.
PATCH/v1/sandboxes/{id}
Change settings. Fields you leave out stay as they are.
- Auth
- API key or session
- Success
200 OK
{ "keep_awake": true, "idle_timeout_minutes": 30, "egress": { "mode": "none" }, "secrets": ["ANTHROPIC_API_KEY"]}ResponseThe updated sandbox.
Errors
404 not_foundno sandbox sb-…400 invalid_requestidle_timeout_minutes must be 1-1440
- secrets replaces the attached set; [] detaches all.
- An egress change applies to new connections. egress_pending stays true until the host confirms it enforces the new policy.
- Never wakes a sandbox.
DELETE/v1/sandboxes/{id}
Delete a sandbox with its files and memory.
- Auth
- API key or session
- Success
200 OK
ResponseThe sandbox, with "status": "deleted", or "deleting" if the host has not confirmed yet (we keep trying).
Errors
404 not_foundno sandbox sb-…409 conflictthe sandbox is creating; delete it once that finishes
- Its snapshots are deleted with it. This cannot be undone.
- A pause, resume or create in progress gets time to finish first. Deleting a deleted sandbox succeeds.
POST/v1/sandboxes/{id}/pause
Pause now: memory and files are saved, nothing runs.
- Auth
- API key or session
- Success
200 OK
ResponseThe sandbox, with "status": "paused".
Errors
404 not_foundno sandbox sb-…409 conflictcannot pause a sandbox that is stopped
POST/v1/sandboxes/{id}/resume
Resume a paused or stopped sandbox. Any use resumes it too.
- Auth
- API key or session
- Success
200 OK
ResponseThe sandbox, with "status": "running".
Errors
404 not_foundno sandbox sb-…429 quota_exceededthis account may run at most N sandboxes at once; pause or delete one503 unavailablethe sandbox's host is busy; retry shortly503 unavailablethe sandbox is still pausing or resuming; retry shortly
- A stopped sandbox boots fresh from its files: they are kept, processes are not, and cold_booted_at is set.
Commands
A command runs as the user agent with a clean environment and no standard input. It keeps running when you disconnect, and its output is kept in the sandbox so you can reattach.
POST/v1/sandboxes/{id}/execs
Start a command. Returns once it is running.
- Auth
- API key or session
- Success
201 Created
{ "argv": ["sh", "-c", "npm test && npm run build"], "cwd": "/workspace/app", "env": { "CI": "1" }}{ "id": "ex-7c2d90a1e4b35f8d6a01", "sandbox_id": "sb-4f1c9e27a0b3d5c86e12", "pid": 412 }Errors
404 not_foundno sandbox sb-…400 invalid_requestargv must not be empty429 quota_exceededthis account may run at most N sandboxes at once; pause or delete one
- argv is passed to the program as-is. For shell syntax, ask for a shell: ["sh", "-c", "…"].
- cwd defaults to /workspace. Your env is applied on top of the sandbox's secret placeholders.
- Resumes the sandbox if it is paused or stopped. A resume counts toward max_running, so this can answer 429.
GET/v1/sandboxes/{id}/execs/{exec_id}/events
Stream a command's output from a byte offset, then its exit, as newline-delimited JSON.
- Auth
- API key or session
- Success
200 OK, Content-Type: application/x-ndjson
Query
- after
- Byte offset to start from (default 0). Pass the offset of the last byte you received to resume.
{"type":"output","stream":"stdout","offset":0,"data":"aGVsbG8K"}{"type":"output","stream":"stderr","offset":6,"data":"d2FybmluZwo="}{"type":"truncated","retained_from":1048576}{"type":"exited","code":0,"signal":null,"output_bytes":14}Errors
404 not_foundno such exec400 invalid_requestafter must be a byte offset
- See Streaming output below for how offsets work.
- Open streams count as activity, so the sandbox stays awake while you watch.
- Resumes the sandbox if it is paused or stopped. A resume counts toward max_running, so this can answer 429.
POST/v1/sandboxes/{id}/execs/{exec_id}/signal
Signal the command and its children.
- Auth
- API key or session
- Success
204 No Content
{ "signal": "interrupt" }- signal is interrupt (SIGINT), terminate (SIGTERM) or kill (SIGKILL).
Files
One file at a time, streamed both ways, up to 1 GiB. The path is an absolute path to a file inside the sandbox.
GET/v1/sandboxes/{id}/export
A gzipped tar archive of a file or directory in the sandbox, as a download.
- Auth
- API key or session
- Success
200 OK
Query
- path
- An absolute path in the sandbox, a file or a directory. Default /workspace.
ResponseThe archive (Content-Type application/gzip, Content-Disposition attachment; filename=SANDBOX.tar.gz).
Errors
404 not_foundno sandbox sb-…403 forbiddenthis account can run no sandboxes; exports stay open for 30 days after a subscription ends, and that window has closed409 conflictan export of this sandbox is already running
- Resumes a paused sandbox. For 30 days after a subscription ends it still works: the sandbox is resumed only for the export and paused again after.
- If the export fails partway the response ends with an error, so a cut-off archive is never taken for a whole one.
PUT/v1/sandboxes/{id}/files
Upload a file. The body is the raw file contents.
- Auth
- API key or session
- Success
200 OK
Query
- path
- Absolute path of the file in the sandbox.required
{ "size": 1832 }Errors
400 invalid_requestpath must be an absolute path to a file400 invalid_requestfile exceeds the 1 GiB transfer limit503 unavailableThe sandbox's disk is full; the message says so.
- Missing parent directories are created. The target is replaced only once the upload completes. New files belong to agent.
- Resumes the sandbox if it is paused or stopped. A resume counts toward max_running, so this can answer 429.
GET/v1/sandboxes/{id}/files
Download a file as application/octet-stream, with its Content-Length.
- Auth
- API key or session
- Success
200 OK
Query
- path
- Absolute path of the file in the sandbox.required
ResponseThe file's bytes.
Errors
404 not_foundThe file does not exist.
- Resumes the sandbox if it is paused or stopped. A resume counts toward max_running, so this can answer 429.
Shell and browser connections
WebSocket connections use a two-step flow, so your API key or session never appears in a URL. See Connecting a shell or browser below.
POST/v1/sandboxes/{id}/connect
Mint a single-use WebSocket URL for a shell or the browser.
- Auth
- API key or session
- Success
200 OK
{ "kind": "shell" }{ "url": "wss://…/v1/connect/ct_…", "expires_at": "2026-10-30T09:31:05.118204Z" }Errors
404 not_foundno sandbox sb-…400 invalid_requestinvalid JSON: …409 conflictthe sandbox is deleted
- kind is shell or browser. The URL works once, within 60 seconds, and only for this sandbox and kind.
GET/v1/connect/{token}
Open the WebSocket. The token in the path is the credential.
- Auth
- The single-use token in the path
- Success
101 Switching Protocols
Query
- cols
- Shell only: terminal width, 1 to 1000 (default 80).
- rows
- Shell only: terminal height, 1 to 1000 (default 24).
Errors
401 unauthorizedmissing, invalid or expired credentials; sign in again503 unavailabletoo many open connections; retry shortly
- 401 means the token is unknown, expired or already used: mint a new one.
- Shell: keystrokes and output are binary messages. Text messages carry JSON: you send {"type":"resize","cols":120,"rows":40}; you receive {"type":"exited","code":0,"signal":null} or {"type":"error","message":"…"}.
- Browser: the WebSocket speaks the Chrome DevTools Protocol. Pass the URL to a CDP client, such as Playwright's connectOverCDP.
- Resumes the sandbox if it is paused or stopped. A resume counts toward max_running, so this can answer 429.
- An open connection keeps the sandbox counted as active.
Network and credentials
GET/v1/sandboxes/{id}/activity
What keeps the sandbox from pausing on its own now.
- Auth
- API key or session
- Success
200 OK
{ "status": "running", "idle_seconds": 0, "idle_timeout_minutes": 10, "keep_awake": false, "reasons": ["waiting for an answer from api.anthropic.com"]}Errors
404 not_foundno sandbox sb-…
- Never wakes a sandbox. Only a running sandbox has reasons or idle_seconds; reasons is empty when nothing keeps it awake.
GET/v1/sandboxes/{id}/egress
The sandbox's recent outbound connections, oldest first.
- Auth
- API key or session
- Success
200 OK
{ "events": [ { "at": "2026-10-30T09:15:02.004113Z", "host": "example.net", "destination": "93.184.215.14:443", "outcome": "denied", "reason": "not on the allowlist", "secrets": [], "count": 1 } ]}- outcome is allowed, denied or failed (allowed, but unreachable). host is the name the connection asked for, or null for protocols that carry none. count is how many similar connections the entry stands for; at is the latest.
- Never wakes a sandbox.
GET/v1/sandboxes/{id}/credentials
The credential inventory: which credentials the sandbox holds and where, never their values.
- Auth
- API key or session
- Success
200 OK
{ "credentials": [ { "kind": "aws_profile", "name": "default", "domain": null, "location": "/home/agent/.aws/credentials" } ]}- kind is one of environment, dot_env, aws_profile, github_cli, git_credentials, netrc, npm, pypi, docker, ssh_key, kubeconfig, gcloud, agent_login, cookies.
- Resumes the sandbox if it is paused or stopped. A resume counts toward max_running, so this can answer 429.
GET/v1/sandboxes/{id}/preview
Open a preview of a web server in the sandbox: a redirect into the preview.
- Auth
- The dashboard's session cookie only
- Success
302 Found
Query
- port
- The port the server listens on, 1 to 65535.required
Errors
403 forbiddenpreviews open only from the signed-in dashboard404 not_foundpreviews are not set up400 invalid_requestport must be 1-65535
- Only the signed-in dashboard can open a preview; API keys and mat sessions get 403.
- The redirect carries a single-use token valid for 60 seconds. The preview then keeps its own cookie, which ends with your dashboard session.
Secrets
Brokered secrets belong to the account. Values can be written and replaced, never read back.
PUT/v1/secrets/{name}
Create or replace a secret.
- Auth
- API key or session
- Success
200 OK
{ "value": "sk-ant-…", "domains": ["api.anthropic.com"] }{ "name": "ANTHROPIC_API_KEY", "domains": ["api.anthropic.com"], "sandboxes": 2, "updated_at": "2026-10-30T09:20:11.730015Z" }Errors
400 invalid_request"…" is not a valid secret name: use letters, digits and _400 invalid_requestvalue must be 1-8192 bytes of printable ASCII400 invalid_requestbind 1-16 domains400 invalid_request"…" is not a domain or *.domain
- name is letters, digits and _, not starting with a digit, up to 128 characters. It is also the environment variable the placeholder appears as.
- Replacing a value updates every sandbox it is attached to. sandboxes counts them.
GET/v1/secrets
List secrets: names, domains and use, never values.
- Auth
- API key or session
- Success
200 OK
{ "secrets": [ { "name": "ANTHROPIC_API_KEY", "domains": ["api.anthropic.com"], "sandboxes": 2, "updated_at": "…" } ] }DELETE/v1/secrets/{name}
Delete a secret and detach it from every sandbox.
- Auth
- API key or session
- Success
204 No Content
Errors
404 not_foundno secret NAME
Account and API keys
GET/v1/me
Who is calling, and the account's limits and current use.
- Auth
- API key or session
- Success
200 OK
{ "account_id": "acct-5d2e8a41c09b7f36", "user": { "name": "Ada Lovelace", "email": "ada@example.com", "provider": "github" }, "limits": { "max_sandboxes": 50, "max_memory_mib": 204800, "max_running": 10, "sandboxes": 3, "memory_mib": 10240, "running": 1 }}- user is null when calling with an API key. sandboxes and memory_mib count every sandbox not deleted, paused ones included; running counts those creating, running, resuming or pausing.
GET/v1/keys
List the account's API keys, never their secrets.
- Auth
- Signed-in session only; API keys get 403
- Success
200 OK
{ "keys": [ { "id": "key-1a2b3c4d5e6f7a8b", "label": "ci", "created_at": "…" } ] }POST/v1/keys
Create an API key. Its secret is shown only in this response.
- Auth
- Signed-in session only; API keys get 403
- Success
201 Created
{ "label": "ci" }{ "id": "key-1a2b3c4d5e6f7a8b", "label": "ci", "key": "mk_…", "created_at": "…" }Errors
400 invalid_requestlabels are up to 64 printable characters403 forbiddensign in to do this; API keys cannot
DELETE/v1/keys/{id}
Revoke an API key at once.
- Auth
- Signed-in session only; API keys get 403
- Success
204 No Content
Errors
404 not_foundno API key key-…
Billing
GET/v1/billing
Billing status and this calendar month's usage (UTC), in hours per size.
- Auth
- API key or session
- Success
200 OK
{ "status": "active", "plan": "pay_as_you_go", "month": "2026-10", "usage": [ { "size": "small", "running_hours": 0.0, "paused_hours": 0.0 }, { "size": "medium", "running_hours": 41.25, "paused_hours": 310.5 }, { "size": "large", "running_hours": 0.0, "paused_hours": 0.0 } ], "can_manage": true}- status is off (billing is not set up on this deployment), none (no payment method), pending (checkout finished, subscription not active yet), active, past_due (a payment failed and Stripe is retrying) or ended (cancelled or unpaid).
- plan is pay_as_you_go while subscribed, otherwise null. paused_hours covers paused and stopped time, billed as storage. can_manage says whether the billing portal can open.
POST/v1/billing/checkout
A Stripe Checkout page to add a payment method and subscribe.
- Auth
- Signed-in session only; API keys get 403
- Success
200 OK
{ "url": "https://checkout.stripe.com/…" }Errors
403 forbiddensign in to do this; API keys cannot404 not_foundbilling is not set up here409 conflictthis account already has a subscription; use Manage billing
POST/v1/billing/portal
Stripe's billing portal, for cards and invoices.
- Auth
- Signed-in session only; API keys get 403
- Success
200 OK
{ "url": "https://billing.stripe.com/…" }Errors
403 forbiddensign in to do this; API keys cannot404 not_foundbilling is not set up here409 conflictadd a payment method first
Signing in from a CLI
The device flow mat login uses. You only need it to build your own client for people; programs should use API keys.
POST/v1/auth/cli
Start a sign-in. Show the person user_code and verification_url.
- Auth
- None
- Success
200 OK
{ "device_code": "md_…", "user_code": "KQTR-WMZD", "verification_url": "https://…/cli?code=KQTR-WMZD", "expires_at": "2026-10-30T09:22:00Z", "interval_seconds": 3}- Keep device_code to yourself; it collects the session. The code expires after 10 minutes.
POST/v1/auth/cli/token
Poll every interval_seconds until the person confirms the code in the dashboard.
- Auth
- None
- Success
200 OK
{ "device_code": "md_…" }{ "status": "pending" }{ "status": "approved", "token": "ms_…", "expires_at": "2026-11-06T09:12:00Z" }{ "status": "expired" }- The token is returned once and lasts 7 days. Expired means expired, already used or unknown: start again.
POST/v1/auth/logout
End the calling session.
- Auth
- Signed-in session only; API keys get 403
- Success
204 No Content
Errors
403 forbiddenAPI keys are revoked, not signed out
Snapshots
New and still being finished; details may change. See Snapshots and fork for what each operation keeps.
POST/v1/sandboxes/{id}/snapshotsnew
Take a snapshot of the sandbox's memory and files. Returns once it is ready.
- Auth
- API key or session
- Success
201 Created
{ "name": "before-migration" }{ "id": "snap-0b9e4a7c21d85f3e6a14", "sandbox_id": "sb-4f1c9e27a0b3d5c86e12", "name": "before-migration", "status": "ready", "allocated_bytes": 431980544, "freeze_ms": 240, "created_at": "2026-10-30T09:40:12.031877Z"}Errors
404 not_foundno sandbox sb-…400 invalid_requesta snapshot name is 1-64 characters, without control characters409 conflictthe sandbox is stopped and has no memory to save; use it to boot it first429 quota_exceededa sandbox keeps at most 10 snapshots; delete one first503 unavailablethe sandbox's host has no disk for a snapshot right now; retry later
- The body is optional; an empty body means no name.
- allocated_bytes is the disk the snapshot holds on its host, counting parts it still shares with the sandbox. freeze_ms is how long a running sandbox was frozen, 0 if it was paused.
GET/v1/sandboxes/{id}/snapshotsnew
List the sandbox's snapshots.
- Auth
- API key or session
- Success
200 OK
{ "snapshots": [ { "id": "snap-0b9e4a7c21d85f3e6a14", "status": "ready", … } ] }Errors
404 not_foundno sandbox sb-…
- status is creating, ready, failed (it could not be taken, or its files were lost) or deleted.
DELETE/v1/sandboxes/{id}/snapshots/{snapshot_id}new
Delete one snapshot.
- Auth
- API key or session
- Success
200 OK
ResponseThe snapshot, with "status": "deleted".
Errors
404 not_foundno snapshot snap-…409 conflictthe snapshot is still being taken; delete it once it is ready
POST/v1/sandboxes/{id}/restorenew
Roll the sandbox back to one of its snapshots, memory and files. It ends paused at that moment.
- Auth
- API key or session
- Success
200 OK
{ "snapshot_id": "snap-0b9e4a7c21d85f3e6a14" }ResponseThe sandbox, with "status": "paused".
Errors
404 not_foundno snapshot snap-…409 conflictthe snapshot is creating, not ready409 conflictthe sandbox is resuming; restore it once that finishes409 conflictthe snapshot's files are gone
- The next exec, file or connect call resumes the sandbox with the processes it had at the snapshot.
POST/v1/snapshots/{snapshot_id}/forknew
Create a new sandbox from the snapshot's files. It boots fresh: files are kept, processes are not.
- Auth
- API key or session
- Success
201 Created
Headers
- Idempotency-Key
- 1 to 128 visible ASCII characters. Retrying with the same key returns the fork the first request made.
ResponseThe new sandbox. Its "forked_from" is the snapshot ID.
Errors
404 not_foundno snapshot snap-…409 conflictthe snapshot is creating, not ready409 conflictthis Idempotency-Key was already used for another request403 forbiddenthis sandbox is held for review after suspected abuse (port scanning) and cannot run until it is cleared; contact support409 conflictthe sandbox the snapshot came from is deleting429 quota_exceededtoo many sandboxes created this minute; retry in N seconds503 unavailablethe snapshot's host takes no new sandboxes now; retry later503 unavailableno capacity available right now on the snapshot's host; retry shortly
- No body. The fork gets the source's size and its idle, egress and secret settings.
- It counts as a create: the create rate limit and your quotas apply, with the same errors as POST /v1/sandboxes.
- It is placed on the snapshot's host, because that is where the files are.