Sign in

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

WhatControl planeEdge
Base URLhttps://api.prized.dev/api/v1The 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).

KeyReaches
Full accessEvery route below
Read only, or Custom with Sandboxes: ReadGET routes: templates, the list, one sandbox
Custom, Sandboxes: Write, or an agent sign-in keyEvery route below, the edge included
Limited to some boxesNone: 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

TopicRule
JSONSend Content-Type: application/json on the control plane. A field the route does not know is 422 unknown_field with field, never ignored.
IdsSandboxes are sbx_…, forks frk_…. An unknown, malformed, deleted, or foreign id is 404 not_found.
202 means in progressPause, 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).
IdempotencyPOST /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.
UnitsmemoryMb is MiB, diskGb is GiB, timers are whole minutes, timestamps are RFC 3339 UTC.
StabilityFields are added without notice; ignore the ones you do not know.

Templates

RouteAnswer
GET /api/v1/sandbox-templates200 {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).

FieldDefaultRule
templateRequiredA template id from GET /sandbox-templates
resourcesThe 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
envVarsNoneUp 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)
labelsNoneUp to 32 string pairs (keys up to 64 characters, values up to 256), yours to filter on
autoPauseMinOffPause after this many idle minutes; 0 is off, up to 40,320
autoDeleteMinOffDelete after this many minutes paused; 0 is off, up to 40,320
maxRuntimeMinThe template's defaultMaxRuntimeMin (1,440); 60 on the free tierDelete 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"
  }
}
FieldMeaning
statestarting, running, pausing, paused, resizing, deleting, deleted, or error (States)
templateThe template name it was created from
templateBuildThe build of that template it runs, which it keeps for life: a later republish of the name changes new sandboxes only
durabilityAlways ephemeral
expiresAtWhen maxRuntimeMin deletes it
errorReasonWhy it is in error; otherwise null
autoPauseMin, autoDeleteMinnull when off
toolboxBaseThe edge address for this sandbox's commands and files
lastActiveAtThe last activity autoPauseMin counts from

Read

RouteAnswer
GET /api/v1/sandboxes200 {sandboxes: [...]}: every sandbox not deleted, newest first
GET /api/v1/sandboxes/{id}200 {sandbox}, or 404 not_found once it is deleted

Lifecycle

RouteBodyAnswer
POST /api/v1/sandboxes/{id}/pause202 {sandbox}, state: "pausing"; with ?wait=paused, 200 once it is paused
POST /api/v1/sandboxes/{id}/resume202 {sandbox}, state: "starting"; with ?wait=running, 200 once it is running. Checks the balance and the quota like a create
POST /api/v1/sandboxes/{id}/refresh204: counts as activity for autoPauseMin
POST /api/v1/sandboxes/{id}/fork{count, labels?}202 {fork, sandboxes}, below
POST /api/v1/sandboxes/{id}/resize{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"
QueryRule
waitrunning on create and resume, paused on pause; anything else is 400 bad_request with field: "wait"
waitTimeoutMsHow 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, create 201, with the sandbox in its new state.
  • It did not get there in time. The answer is the one without wait (202, or 201 for a create), with the sandbox as it is by then, still starting or pausing: read it until it settles, as without wait. A create that boots from scratch can outlast the hold.
  • It failed. The hold ends at once, with the sandbox in error or on its way there; the status is the one without wait.
  • 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 (pausing or starting, or still running or paused if it had not moved yet). The status is the one without wait.
  • A pause sent while the sandbox is still starting holds through the boot: it shows starting, then pausing, and answers 200 once it is paused (or as without wait at 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 starting until the pause has landed and it runs again; pause one still resuming and it shows pausing until it is paused. 200 comes 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 /resume would, then waits for running and 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_ready with Retry-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_running at once and wakes nothing. Read the sandbox until it leaves resizing, then retry.
  • Prized-Auto-Resume: false on a request turns waking off for it: a sandbox that is not running answers 409 sandbox_not_running, and nothing wakes. On a running sandbox 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, read GET /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

RouteBodyAnswer
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 -c as the user prized, in cwd (absolute; /home/prized/work by default), with the create's envVars and then this request's env on top.
  • The answer is the verdict. A command that fails still answers 200; read exitCode.
  • timeout is 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

RouteBody or queryAnswer
POST /files/uploadquery path; the raw file as the body200 {path, size}
GET /files/downloadquery pathThe 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.json

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

HTTPerrorMeaning
400bad_requestThe 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)
400invalid_idempotency_keyNot 1 to 128 printable characters
401unauthorizedNo key, or a revoked one
401token_expiredThe key has expired, with expiresAt
403key_access_deniedThe key lacks Sandboxes: Read or Write, or is limited to some boxes; message names what it needs
404not_foundNo such sandbox in this workspace, a contractor's key, or a create, fork, or resize while sandboxes are switched off for this workspace (email )
404template_not_foundtemplate is missing or names no template
409paid_balance_requiredFree-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
409insufficient_balanceThe balance is empty
409workspace_heldThe 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
409workspace_closingThe workspace is being closed, on a create, resume, or fork; the owner can reopen it (Close or erase the workspace)
409template_inactiveThe template is retired; pick another
409idempotency_conflictThe key was used for another request
409durability_incompatibledurability: "persistent"; every sandbox is ephemeral
409parent_not_runningFork of a sandbox that is not running, with its state
409parent_not_forkableThis 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
409fork_capacityThe 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
409fork_unsupported_hostThis sandbox's machine cannot fork; create a fresh sandbox and fork that
409resize_rejectedWith reason: not_stopped (pause it first), resize_in_progress, lifecycle_in_progress, unchanged, or host_capacity
422unknown_fieldA field the route does not take, named in field
422invalid_resourcesA 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)
422disk_shrink_unsupportedA resize to a smaller disk
422invalid_max_runtimemaxRuntimeMin is not a positive whole number
422invalid_countA fork count outside 1 to max (256)
422invalid_labelsFork labels that are not up to 32 string pairs
429sandbox_quota_exceededA 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
503sandbox_capacityNo sandbox capacity is free right now. Nothing was created or billed; retry after Retry-After (also retryAfterSec in the body)
503sandboxes_disabled, sandbox_create_disabled, sandbox_fork_disabledSandboxes, or creating or forking them, are paused for maintenance; retry later

The edge:

HTTPerrorMeaning
400bad_json, missing_command, cwd_not_absolute, bad_timeout, bad_envThe exec document is not the documented shape; detail says more where it can
400spawn_failedThe command could not start (a cwd that does not exist); detail has the system's words
400missing_path, path_not_absolute, symlink_not_allowed, not_regular_fileThe path is missing, relative, through a symlink, or not a regular file
401unauthorized, token_expiredAs on the control plane
403key_access_deniedThe key lacks Sandboxes: Write
403path_outside_root, permission_deniedThe path is outside /home/prized, or prized cannot touch it
404not_foundNo such sandbox for this key, or no such file
409sandbox_not_runningThe 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, 429The 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
413body_too_large, file_too_largeOver 1 MiB for an exec document, or 100 MiB for a file
429rate_limitedOver the edge limits; Retry-After says when
502sandbox_unreachableThe sandbox did not answer. An exec may or may not have run
502resolve_failed, sandbox_credentials_unavailableThe edge could not look the sandbox up; retry
503sandbox_not_readyThe sandbox did not reach running within the edge's 10-second wait (The edge); nothing ran. Retry after Retry-After
500control_plane_unavailableThe edge could not reach the control plane to check the key

Not yet

Not yetInstead
Streaming output, or a terminalWrite output to a file and read it with a later exec or a download
Listing, moving, or deleting files through the edgels, mv, and rm through exec
WebhooksPoll GET /api/v1/sandboxes/{id}
An OpenAPI document, or TypeScript and Python SDKsThe routes above with curl or any HTTP client, or a helper for Python or TypeScript