Sandbox API
Every sandbox route: create, list, pause, resume, fork, resize, and delete on the control plane; run commands and move files on the edge; with the fields, states, and error codes.
On this page16
What a sandbox is, and how its lifecycle works, is on Sandboxes. The routes for boxes and the workspace are on API reference.
Two hosts, one key
| What | Control plane | Edge |
|---|---|---|
| Base URL | https:/ | The sandbox's toolboxBase |
| Routes | /sandboxes, /sandbox-templates | /process/execute, /files/upload, /files/download |
| Errors | {"error": "code"}, sometimes with reason, field, or message | {"error": "code"}, sometimes with detail or message |
Read toolboxBase from the sandbox object rather than building it; it is the same for the sandbox's whole life.
export PRIZED_API_KEY=dcp_…
API=https://api.prized.dev/api/v1
AUTH="Authorization: Bearer $PRIZED_API_KEY"Authentication
Send Authorization: Bearer dcp_… on every request, with an API key from Dashboard → Settings → API keys (API keys). Expiry, revocation, and rotation work as on the rest of the API (Authentication).
| Key | Reaches |
|---|---|
| Full access | Every route below |
| Custom, Sandboxes: Read | GET routes: templates, the list, one sandbox |
| Custom, Sandboxes: Write | Every route below, the edge included |
| Limited to some boxes, or an agent sign-in key | None: 403 key_access_denied |
A key acts as the member who made it: an owner's or member's key sees every sandbox in the workspace, and a contractor's gets 404 not_found on every sandbox route. On the free credit alone, a workspace works within the free tier's limits.
Conventions
| Topic | Rule |
|---|---|
| JSON | Send Content-Type: application/json on the control plane. A field the route does not know is 422 unknown_field with field, never ignored. |
| Ids | Sandboxes are sbx_…, forks frk_…. An unknown, malformed, deleted, or foreign id is 404 not_found. |
| 202 means in progress | Pause, resume, fork, resize, and delete answer 202 with the sandbox as written; its state moves within seconds. Poll GET /sandboxes/{id}. |
| Idempotency | POST /sandboxes and POST /sandboxes/{id}/fork take an Idempotency-Key header (1 to 128 printable characters): a retry with the same key and body answers 200 with the first result, and makes nothing. The same key with another body is 409 idempotency_conflict. |
| Units | memoryMb is MiB, diskGb is GiB, timers are whole minutes, timestamps are RFC 3339 UTC. |
| Stability | Fields are added without notice; ignore the ones you do not know. |
Templates
| Route | Answer |
|---|---|
GET / | 200 {templates: [...]}, each {id, resources: {defaults, min, max}, durability, defaultMaxRuntimeMin} |
{
"templates": [
{
"id": "prized-base-v1",
"resources": {
"defaults": {"vcpu": 4, "memoryMb": 8192, "diskGb": 10},
"min": {"vcpu": 1, "memoryMb": 1024, "diskGb": 10},
"max": {"vcpu": 8, "memoryMb": 16384, "diskGb": 100}
},
"durability": "ephemeral",
"defaultMaxRuntimeMin": 1440
}
]
}What each template holds is on Sandboxes: Templates.
Create
POST /api/v1/sandboxes answers 201 {sandbox} with state: "starting"; read it until running.
curl -s -X POST "$API/sandboxes" -H "$AUTH" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" -d '{"template":"prized-agent-v1","resources":{"vcpu":2,"memoryMb":4096},"envVars":{"ANTHROPIC_API_KEY":"sk-ant-…"},"labels":{"run":"pr-1874"},"autoPauseMin":10,"autoDeleteMin":60,"maxRuntimeMin":120}'| Field | Default | Rule |
|---|---|---|
template | Required | A template id from GET /sandbox-templates |
resources | The template's defaults (capped at 2 vCPU and 4 GB on the free tier) | {vcpu, memoryMb, diskGb}, any of them, each within the template's min and max (and the free tier's caps) |
envVars | None | Up to 32 names (shell identifiers of up to 64 characters), each value up to 16 KiB; set for every command, stored encrypted, never returned (Environment variables) |
labels | None | Up to 32 string pairs (keys up to 64 characters, values up to 256), yours to filter on |
autoPauseMin | Off | Pause after this many idle minutes; 0 is off, up to 40,320 |
autoDeleteMin | Off | Delete after this many minutes paused; 0 is off, up to 40,320 |
maxRuntimeMin | The template's defaultMaxRuntimeMin (1,440); 60 on the free tier | Delete this many minutes after the create; up to the workspace's maximum runtime (1,440, or 60 on the free tier) |
The sandbox object
{
"sandbox": {
"id": "sbx_3f9c0a1b2d4e5f60",
"state": "running",
"template": "prized-agent-v1",
"resources": {"vcpu": 2, "memoryMb": 4096, "diskGb": 10},
"durability": "ephemeral",
"labels": {"run": "pr-1874"},
"autoPauseMin": 10,
"autoDeleteMin": 60,
"expiresAt": "2026-10-14T19:02:11Z",
"errorReason": null,
"toolboxBase": "https://…/v1/sandbox/sbx_3f9c0a1b2d4e5f60",
"createdAt": "2026-10-14T17:02:11Z",
"lastActiveAt": "2026-10-14T17:05:40Z"
}
}| Field | Meaning |
|---|---|
state | starting, running, pausing, paused, resizing, deleting, deleted, or error (States) |
durability | Always ephemeral |
expiresAt | When maxRuntimeMin deletes it |
errorReason | Why it is in error; otherwise null |
autoPauseMin, autoDeleteMin | null when off |
toolboxBase | The edge address for this sandbox's commands and files |
lastActiveAt | The last activity autoPauseMin counts from |
Read
| Route | Answer |
|---|---|
GET /api/v1/sandboxes | 200 {sandboxes: [...]}: every sandbox not deleted, newest first |
GET /api/v1/sandboxes/{id} | 200 {sandbox}, or 404 not_found once it is deleted |
Lifecycle
| Route | Body | Answer |
|---|---|---|
POST / | 202 {sandbox}, state: "pausing" | |
POST / | 202 {sandbox}, state: "starting". Checks the balance and the quota like a create | |
POST / | 204: counts as activity for autoPauseMin | |
POST / | {count, labels?} | 202 {fork, sandboxes}, below |
POST / | {resources} | 202 {sandbox, operation}, below |
DELETE /api/v1/sandboxes/{id} | 202 {sandbox}, state: "deleting"; again on a deleting sandbox is a no-op |
Fork
count is 1 to 256; labels replaces the parent's on every child. The answer lists the children, each starting; poll each until running.
{
"fork": {
"id": "frk_9a1c44e0b7d2f318",
"parentId": "sbx_3f9c0a1b2d4e5f60",
"count": 2,
"status": "active",
"sandboxIds": ["sbx_51d0c2aa9e8b7f64", "sbx_c8e4f1b20a3d9e75"],
"createdAt": "2026-10-14T17:06:02Z"
},
"sandboxes": [{"id": "sbx_51d0c2aa9e8b7f64", "state": "starting", "…": "…"}]
}What a child inherits, and the limits, are on Sandboxes: Fork.
Resize
A paused sandbox only. resources names any of vcpu, memoryMb, diskGb, within the template's bounds; disk never shrinks. The sandbox shows resizing, then paused again; its next resume boots fresh from the resized disk, without the paused memory.
{
"sandbox": {"id": "sbx_3f9c0a1b2d4e5f60", "state": "resizing", "…": "…"},
"operation": {
"id": "…",
"status": "pending",
"resources": {"vcpu": 4, "memoryMb": 8192, "diskGb": 20},
"coldRestart": true,
"pausedMemoryDiscarded": true,
"createdAt": "2026-10-14T17:10:00Z",
"completedAt": null
}
}Read the sandbox until it leaves resizing: paused with the new resources when the resize succeeded, paused with the old ones when it was turned down before it began, or error (with errorReason) when it failed partway, after which the sandbox cannot resume and should be deleted. To read the operation itself, send the resize with an Idempotency-Key and send the same request again: the replay answers 200 with the operation as it is now, its status moving through pending and applying to succeeded, failed (with failureReason), or cancelled.
The edge
Routes below are relative to the sandbox's toolboxBase, with the same bearer. They need a running sandbox: a paused one answers 409 sandbox_not_running, and nothing on the edge wakes it.
Run a command
| Route | Body | Answer |
|---|---|---|
POST /process/execute | {command, cwd?, env?, timeout?} | 200 {stdout, stderr, exitCode, durationMs, timedOut?} |
curl -s -X POST "$TOOLBOX/process/execute" -H "$AUTH" -H "Content-Type: application/json" -d '{"command":"npm test","cwd":"/home/prized/work/app","env":{"CI":"1"},"timeout":600}'
# {"stdout":"…","stderr":"","exitCode":0,"durationMs":8124}- How it runs.
bash -cas the userprized, incwd(absolute;/home/prized/workby default), with the create'senvVarsand then this request'senvon top. - The answer is the verdict. A command that fails still answers
200; readexitCode. timeoutis in seconds, 300 by default and 3,600 at most. At the limit the command and everything it started are killed:exitCode: -1, timedOut: true.- Output. Each stream keeps its first 4 MiB; the rest is dropped. For more, write to a file and download it.
- One run per request. An exec whose answer was lost is never retried for you, since it may already have run; retry it yourself only if the command is safe to run twice.
Files
| Route | Body or query | Answer |
|---|---|---|
POST /files/upload | query path; the raw file as the body | 200 {path, size} |
GET /files/download | query path | The file as application/octet-stream with Content-Length |
curl -s -X POST "$TOOLBOX/files/upload?path=/home/prized/work/app.py" -H "$AUTH" --data-binary @app.py
curl -s "$TOOLBOX/files/download?path=/home/prized/work/out.json" -H "$AUTH" -o out.jsonPaths are absolute and inside /home/prized; a path through a symlink is refused. An upload creates missing directories and replaces the file whole, never half-written. Downloads are regular files only; list or archive a directory with a command (tar czf /home/prized/out.tgz work) and download that. Each transfer is up to 100 MiB and 10 minutes.
Edge limits
48 concurrent requests and 60 a second (bursts of 120) per key, and 8 concurrent and 20 a second (bursts of 40) per sandbox; past either, 429 rate_limited with Retry-After. The request document of an exec is at most 1 MiB.
Errors
The control plane:
| HTTP | error | Meaning |
|---|---|---|
| 400 | bad_request | The body is not a JSON object, or a field has the wrong type or is out of range; for envVars, with field and a message stating the rules |
| 400 | invalid_idempotency_key | Not 1 to 128 printable characters |
| 401 | unauthorized | No key, or a revoked one |
| 401 | token_expired | The key has expired, with expiresAt |
| 403 | key_access_denied | The key lacks Sandboxes: Read or Write, or is limited to some boxes; message names what it needs |
| 404 | not_found | No such sandbox in this workspace, a contractor's key, or a create, fork, or resize while sandboxes are switched off for this workspace (email ) |
| 404 | template_not_found | template is missing or names no template |
| 409 | paid_balance_required | Free-tier access is switched off for your workspace, and it has only free credit, on a create, resume, or fork: add a plan or a top-up |
| 409 | insufficient_balance | The balance is empty |
| 409 | workspace_held | The workspace is on hold (Not allowed), on a create, resume, or fork; message says why. The hold paused its running sandboxes; pausing and deleting still work. Email for a review |
| 409 | workspace_closing | The workspace is being closed, on a create, resume, or fork; the owner can reopen it (Close or erase the workspace) |
| 409 | template_inactive | The template is retired; pick another |
| 409 | idempotency_conflict | The key was used for another request |
| 409 | durability_incompatible | durability: "persistent"; every sandbox is ephemeral |
| 409 | parent_not_running | Fork of a sandbox that is not running, with its state |
| 409 | parent_not_forkable | This sandbox cannot be forked |
| 409 | fork_capacity | The parent's machine has room for only fits more copies; fork that many or fewer |
| 409 | fork_unsupported_host | This sandbox's machine cannot fork; create a fresh sandbox and fork that |
| 409 | resize_rejected | With reason: not_stopped (pause it first), resize_in_progress, lifecycle_in_progress, unchanged, or host_capacity |
| 422 | unknown_field | A field the route does not take, named in field |
| 422 | invalid_resources | A shape outside the template's bounds, or over the free tier's 2 vCPU and 4 GB (then with "tier": "free" and limit, in vCPU or MB), named in field |
| 422 | disk_shrink_unsupported | A resize to a smaller disk |
| 422 | invalid_max_runtime | maxRuntimeMin is not a positive whole number |
| 422 | invalid_count | A fork count outside 1 to max (256) |
| 422 | invalid_labels | Fork labels that are not up to 32 string pairs |
| 429 | sandbox_quota_exceeded | A quota, or a free-tier limit (then with "tier": "free" and limit, in the reason's unit), would be passed, named in reason: sandbox_limit, running_vcpu, running_memory, retained_disk, or max_runtime; on a resume, customer_disabled means sandboxes are switched off for this workspace (email ). A fork adds admitted, how many children would have fit |
| 503 | sandbox_capacity | No sandbox capacity is free right now. Nothing was created or billed; retry after Retry-After (also retryAfterSec in the body) |
| 503 | sandboxes_disabled, sandbox_create_disabled, sandbox_fork_disabled | Sandboxes, or creating or forking them, are paused for maintenance; retry later |
The edge:
| HTTP | error | Meaning |
|---|---|---|
| 400 | bad_json, missing_command, cwd_not_absolute, bad_timeout, bad_env | The exec document is not the documented shape; detail says more where it can |
| 400 | spawn_failed | The command could not start (a cwd that does not exist); detail has the system's words |
| 400 | missing_path, path_not_absolute, symlink_not_allowed, not_regular_file | The path is missing, relative, through a symlink, or not a regular file |
| 401 | unauthorized, token_expired | As on the control plane |
| 403 | key_access_denied | The key lacks Sandboxes: Write |
| 403 | path_outside_root, permission_denied | The path is outside /home/prized, or prized cannot touch it |
| 404 | not_found | No such sandbox for this key, or no such file |
| 409 | sandbox_not_running | The sandbox is not running: resume it and wait |
| 413 | body_too_large, file_too_large | Over 1 MiB for an exec document, or 100 MiB for a file |
| 429 | rate_limited | Over the edge limits; Retry-After says when |
| 502 | sandbox_unreachable | The sandbox did not answer. An exec may or may not have run |
| 502 | resolve_failed, sandbox_credentials_unavailable | The edge could not look the sandbox up; retry |
| 500 | control_plane_unavailable | The edge could not reach the control plane to check the key |
Not yet
| Not yet | Instead |
|---|---|
| Streaming output, or a terminal | Write output to a file and read it with a later exec or a download |
| Listing, moving, or deleting files through the edge | ls, mv, and rm through exec |
| Webhooks | Poll GET /api/v1/sandboxes/{id} |
| An OpenAPI document, or TypeScript and Python SDKs | The routes above with curl or any HTTP client |