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 page17
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 |
| Read only, or Custom with Sandboxes: Read | GET routes: templates, the list, one sandbox |
| Custom, Sandboxes: Write, or an agent sign-in key | Every route below, the edge included |
| Limited to some boxes | 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}, or have the API wait for you (Waiting). |
| 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, build, resources: {defaults, min, max}, durability, defaultMaxRuntimeMin} |
{
"templates": [
{
"id": "prized-base-v1",
"build": "prized-base-v1-r2",
"resources": {
"defaults": {"vcpu": 4, "memoryMb": 8192, "diskGb": 10},
"min": {"vcpu": 1, "memoryMb": 1024, "diskGb": 10},
"max": {"vcpu": 32, "memoryMb": 131072, "diskGb": 128}
},
"durability": "ephemeral",
"defaultMaxRuntimeMin": 1440
}
]
}id is the name a create passes. build is the image a new sandbox of that name starts from today; it changes when the name is republished with a fix (Sandboxes: Templates), and resources are that build's. What each template holds is on the same page.
Create
POST /api/v1/sandboxes answers 201 {sandbox} with state: "starting"; read it until running. With ?wait=running the answer comes once it is running instead (Waiting). A large sandbox (more than 16 vCPU or about 58 GB of memory) may stay starting for a minute or two, longer than the hold: its held create answers 201 still starting, and you read it until running.
curl -s -X POST "$API/sandboxes?wait=running" -H "$AUTH" -H "Content-Type: application/json" -H "Idempotency-Key: pr-1874-1" -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":60}'The Idempotency-Key is yours to choose, one per sandbox: resend the create with the same key and body, and you get the first sandbox back instead of a second (Conventions).
| 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); never more than 32 vCPU, 131,072 MiB, and 128 GiB |
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",
"templateBuild": "prized-agent-v1-r2",
"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) |
template | The template name it was created from |
templateBuild | The build of that template it runs, which it keeps for life: a later republish of the name changes new sandboxes only |
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"; with ?wait=paused, 200 once it is paused | |
POST / | 202 {sandbox}, state: "starting"; with ?wait=running, 200 once it is running. 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 |
Waiting
Create and resume take ?wait=running, and pause ?wait=paused: the API holds the answer until the sandbox gets there, so there is nothing to poll.
curl -s -X POST "$API/sandboxes/sbx_3f9c0a1b2d4e5f60/resume?wait=running" -H "$AUTH"| Query | Rule |
|---|---|
wait | running on create and resume, paused on pause; anything else is 400 bad_request with field: "wait" |
waitTimeoutMs | How long to hold, a whole number of milliseconds from 0 to 8000; default 8000. The answer can come up to half a second after it, while the API takes its last look at the sandbox. Anything else, or waitTimeoutMs without wait, is 400 bad_request with field: "waitTimeoutMs" |
- It got there. Resume and pause answer
200, create201, with the sandbox in its new state. - It did not get there in time. The answer is the one without
wait(202, or201for a create), with the sandbox as it is by then, stillstartingorpausing: read it until it settles, as withoutwait. A create that boots from scratch can outlast the hold. - It failed. The hold ends at once, with the sandbox in
erroror on its way there; the status is the one withoutwait. - It went another way. The hold also ends at once when the sandbox stops heading for that state: deleted (
deleting, or gone, which answers as the request was accepted), or asked the other way by a later pause or resume (pausingorstarting, or stillrunningorpausedif it had not moved yet). The status is the one withoutwait. - A pause sent while the sandbox is still starting holds through the boot: it shows
starting, thenpausing, and answers200once it ispaused(or as withoutwaitat the bound). - A change that reverses one in flight waits for the box, not the request: resume a sandbox that is still pausing and it shows
startinguntil the pause has landed and it runs again; pause one still resuming and it showspausinguntil it is paused.200comes only once it is really there.
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 run on a running sandbox, and wake one that is not:
- Paused or pausing: the request resumes it, as
POST /resumewould, then waits forrunningand runs. The resume is billed and counted as activity like any other, and a refused one answers with the resume's own error (409 insufficient_balance,409 paid_balance_required,429 sandbox_quota_exceeded, …), with nothing run. - Starting or resuming: the request waits for
running, then runs. - The wait is up to 10 seconds, counted from when the edge, having checked the key and looked the sandbox up, finds it not running. Requests with the same key that arrive during a wait share it, deadline included, so a later one waits only what is left of it. Past the deadline the answer is
503 sandbox_not_readywithRetry-After: 1; nothing ran, so retry. - A waiting request counts toward the sandbox's edge limits as a running one does: past 8 at once, the rest answer
429 rate_limited. - Resizing: a request answers
409 sandbox_not_runningat once and wakes nothing. Read the sandbox until it leavesresizing, then retry. Prized-Auto-Resume: falseon a request turns waking off for it: a sandbox that is notrunninganswers409 sandbox_not_running, and nothing wakes. On arunningsandbox the header changes nothing: the request runs and counts as activity, so it still holds off automatic pause. To watch a sandbox without keeping it up, readGET /api/v1/sandboxes/{id}, which is not activity.
Right after a sandbox reaches running, the first request can still answer 502 sandbox_unreachable (it may have run) while the edge's tunnel comes up. So when a create or resume returns, first send a harmless command, such as true, until it answers 200, as the SDK and CLI do; then send the real one.
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 in its process group are killed:exitCode: -1, timedOut: true. A process that left the group (setsid) runs on until it exits or the sandbox is deleted.- 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. A request waiting for its sandbox to wake is one of them. 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; for a wait or waitTimeoutMs the route cannot take, with field naming it (Waiting) |
| 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. With reason parent_too_big, its machine cannot hold even one copy beside it: field names what does not fit (vcpu, memoryMb, or diskGb) and message says so; fork a smaller sandbox |
| 409 | fork_capacity | The parent's machine has room for only fits more copies, with a message saying what to do: fork that many or fewer (when fits is above 0), try again later, or start a new sandbox and fork that, keeping the parent until it runs. The new sandbox and each copy count toward your quota: at the limit, delete sandboxes you no longer need and wait until each answers 404 (a deleting sandbox still counts), or top up on the free tier; never delete a parent while a copy forked from it is still starting |
| 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; field host_capacity means that shape cannot run right now (large sandboxes are not available) |
| 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 and the request cannot wake it: it is resizing (retry once it leaves resizing), failed, or being deleted, or the request sent Prized-Auto-Resume: false |
| 409, 429 | The resume's codes (insufficient_balance, paid_balance_required, sandbox_quota_exceeded, …) | The request needed to resume a paused sandbox, and the resume was refused; nothing ran |
| 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 |
| 503 | sandbox_not_ready | The sandbox did not reach running within the edge's 10-second wait (The edge); nothing ran. Retry after Retry-After |
| 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, or a helper for Python or TypeScript |