API reference.
Every HTTP route a CLI token can call. The control plane at api.prized.dev/api/v1 creates and manages boxes, snapshots, environments and secrets; the edge runs commands, moves files, prompts the box's agent and stops its desktop. No SDK and no OpenAPI document yet; this page is the contract.
Two hosts, one token
| Host | What lives there | A box is addressed by | Error envelope |
|---|---|---|---|
https://api.prized.dev/api/v1 | The control plane: boxes, snapshots, environments, secrets, deletion operations, your account | Its id (a uuid) | {"error": "code"}, sometimes with message |
$EDGE/v1/box/{box}/ | The edge: run commands, read and write files, prompt the agent, stop the desktop. $EDGE comes from GET /api/v1/me | Its name or its id | {"ok": false, "error": {"code": "…", "message": "…"}} |
Both take the same bearer. https://prized.dev/api/v1 reaches the same control plane through the website's proxy. The edge is a different machine (it holds the tunnel to your box), so always read its address from edge.url rather than assuming one.
export PRIZED_TOKEN=dcp_…
API=https://api.prized.dev/api/v1
AUTH="Authorization: Bearer $PRIZED_TOKEN"
EDGE=$(curl -s "$API/me" -H "$AUTH" | jq -r .edge.url)GET https://api.prized.dev/healthz needs no token and answers {"ok": true, "sourceCommit": "…", "controlPlaneApiSha256": "…"}.
Authentication
Every request carries Authorization: Bearer dcp_…. The token is a CLI token: Dashboard → Workspace → CLI tokens mints one (prized login opens that page), and prized login --token dcp_… is how a machine without a browser signs in. Mint one per script or agent and name it, so it can be retired on its own. There is no route that mints a token with a token: minting, listing and revoking are dashboard-only, see Workspaces: CLI tokens.
- What a token can do. It acts as you, in the workspace it was minted in: every box, snapshot, environment and secret there. It cannot change who is in the workspace, mint credentials, or close the workspace. Billing and plan changes are dashboard flows and are not part of this reference.
- Revoking one takes effect on its next request (401), and closes the tunnels that device opened.
- Other credential classes. The dashboard's terminal tickets (
dtt_…, ten minutes, bound to one box) are accepted on exactly these routes:GET /api/v1/boxes,GET /api/v1/boxes/{id},POST /api/v1/boxes/{id}/wake,POST /api/v1/connect,POST /api/v1/disconnect,POST /api/v1/ssh-cert, and the edge's exec, files and prompt routes; they see only their one box. Everything else on this page,POST /desktop/stopincluded, takes adcp_token. Monitor tickets and phone passwords are refused everywhere on this page. The dashboard itself uses its session cookie on the same routes. - The version header. The CLI sends
X-Prized-CLI-Version; a request that sends one below the control plane's minimum answers426 cli_update_requiredwithminVersion. Scripts should not send it.
Conventions
- JSON in, JSON out. Send
Content-Type: application/json. Bodies are capped at 64 KiB; the secrets routes take 128 KiB, and environment create, patch, files and repos take 2 MiB (toggles, vars and upgrade stay at 64 KiB); over the cap, or not JSON, reads as400 bad_request. A204has no body. - Ids. Control-plane routes take the box's
id(a uuid) in the path, never its name;GET /api/v1/boxesmaps names to ids. Snapshots aresnp_…, environmentsenv_…, secretssec_…, deletion operationsdop_…. An id that is unknown, malformed, or belongs to another workspace is404 not_found, with nothing written. - 202 means the reconciler is on it. Creating, waking, pausing, resizing, moving, forking, restoring and deleting a box answer
202with the row as written:desiredStatehas moved andobservedStatefollows over the next seconds to minutes. PollGET /api/v1/boxes/{id}untilobservedStateis what you asked for, orGET /api/v1/operations/{id}for a delete. Setting writes (PATCH, templates, secrets, environments) answer200or201because nothing is left to converge. - Errors. The control plane answers
{"error": "code"}, plusmessageon the refusals that have a reason and extra fields where a client needs numbers (insufficient_balancecarriesbalanceMicros,requiredMicros,burnMicrosPerHour;snapshot_limitcarrieslimit;box_not_runningcarriesstate). The edge answers{"ok": false, "error": {"code", "message"}}. Codes are stable words; messages are for people. - Idempotency. No route takes an idempotency key. A
POST /api/v1/boxesretried after a lost response creates a second box; give the box anameand the retry answers409 name_takeninstead, thenGET /api/v1/boxesfinds the one that was made. Deletes are idempotent (the same operation comes back), and wake or suspend on a box already in that state is a202no-op. - Rate limits. The control plane has no request-rate limiter; its limits are counts:
409 box_limit(the plan's box count),429 snapshot_limit(Limits: Snapshots),402 secret_limit, and429 customer_session_limiton the CLI's tunnel sessions. The edge admits 8 concurrent requests and 20 per second per token and per box, and answers429 rate_limitedwithRetry-After; see Limits: Commands and files. - Times and money. Timestamps are RFC 3339 UTC. Balances are in micro-dollars (
1000000is $1). - Stability. This API is what the CLI and the dashboard use, so every route here is exercised daily, and the box object is the one
prized … --jsonprints. Fields and routes are added under/v1without notice; ignore fields you do not know. Nothing beyond that is promised yet.
Account
| Route | Answer |
|---|---|
GET /api/v1/me | customer (id, email, name, githubLogin, status, onboardingStep, onboardingCompletedAt, createdAt), user (who the token acts as: email, name, role; null for a token with no user), workspace (id, name, zeroDataRetention, closingAt, purgeAfter), billing (balanceMicros, burnMicrosPerHour, runwayHours, warnBelowHours, planAmountUsd), boxes (every box, terminated included), edge.url |
customer.id is the workspace id. box (the oldest live box) is also present but deprecated; read boxes.
Boxes
| Route | Body | Answer |
|---|---|---|
GET /api/v1/boxes | 200 {boxes}, newest first, terminated ones included (filter on observedState) | |
POST /api/v1/boxes | {name?, tier?, ttlMinutes?, autoPauseMin?, environment?, env?, restricted?, fromSnapshotId?, fromTemplate?} | 202 {box} with observedState: "requested"; poll until running |
GET /api/v1/boxes/{id} | 200 {box, state, vitals, events, job, pausesSurvived, environment, envVars, restricted} | |
PATCH /api/v1/boxes/{id} | {autoPauseMin?, ttlMinutes? | pauseAt?, autoSnapshotHours?, autoSnapshotKeep?} or, alone, {env} | 200 {box} |
DELETE /api/v1/boxes/{id} | 202 {box, operation}; see Operations | |
POST /api/v1/boxes/{id}/wake | 202 {box}, desiredState: "running" | |
POST /api/v1/boxes/{id}/suspend | 202 {box}, desiredState: "suspended" | |
POST /api/v1/boxes/{id}/resize | {target_tier} | 202 {box} with the new tier; the box restarts |
POST /api/v1/boxes/{id}/region | {region} (us-west-2 or us-west-1) | 202 {box} with desiredRegion set; region changes when the move lands |
POST /api/v1/boxes/{id}/fork | {name?, tier?, keepSnapshot?} | 202 {box, snapshot} |
POST /api/v1/boxes/{id}/template | {name, description?} | 201 {snapshot} (a template of this box, taken now) |
GET /api/v1/boxes/{id}/metrics | query from, to (epoch seconds or …Z ISO 8601; default the last hour, clamped to 30 days) | 200 {v, res, from, to, buckets, series, fs, ifaces}; res is 30, 300 or 3600 seconds by span |
GET /api/v1/boxes/{id}/procs-at | query t (required) | 200 {v, at, procs, ports}: the process and port snapshot nearest t, within five minutes, else 404 |
The box object is the one on CLI: The box object, plus fields the CLI does not print: kind (devbox), networkKind, autoSnapshotHours, autoSnapshotKeep, environmentId, environmentVersionNo, envVars (the per-box vars as [{name, value}]) and restricted. desiredState is running, suspended or terminated. observedState is the raw word, one of requested, provisioning, bootstrapping, running, suspending, suspended, waking, deep_sleep, resizing, migrating, degraded, provision_failed, terminating, terminated; Boxes: States says what each means.
Creating. name is a hostname (lowercase letters, digits and hyphens, starting and ending with a letter or digit, 1 to 32 characters); without one you get a fruit. tier is a size id (nano, micro, lite, flow, pro, max, ultra; see CLI: Sizes); without one the control plane picks what the balance runs comfortably. ttlMinutes (5 to 43200) is a pause deadline, autoPauseMin (30 to 10080, or null) the idle window. environment is an environment id or name, null for none, absent for your default; env is {NAME: "value"}; restricted makes the box safe for third parties. fromSnapshotId or fromTemplate (one, not both) restores instead of booting fresh, and tier may then be any size whose disk holds the snapshot.
The detail answer. events is the last 50 state changes as {id, fromState, toState, reason, actor, at}; job is the reconciler job in flight (op, step, attempt, lastError, nextRetryAt) or null; vitals is the last heartbeat (cpu_pct, mem_pct, disk_pct, idle_for_sec, listeners_count, agent_version); environment is {id, name, versionNo, latestVersionNo} or null.
Patching takes one family per request: the pause settings and the snapshot schedule together (autoPauseMin 30 to 10080 or null; ttlMinutes or pauseAt, null to clear, only one of the two; autoSnapshotHours 1 to 168 or null; autoSnapshotKeep 1 to 10), or {env: {NAME: "value"}} alone, which replaces the per-box vars (100 names, 64 KiB in all). A deadline needs a running box (409 box_not_running); clearing one always works.
| HTTP | error | When |
|---|---|---|
| 400 | bad_request, invalid_name, invalid_template, invalid_tier, invalid_env, invalid_auto_pause, invalid_pause_at, invalid_auto_snapshot, environment_too_large | The body is off; message names the window or the rule where there is one |
| 400 | resize_rejected, move_rejected | A refusal on a box you own (mid-transition, a fifth move in a day, a region the fleet cannot launch in); message says why |
| 403 | restore_tier_not_allowed | The size is off your plan or its disk cannot hold the snapshot |
| 404 | not_found | No such box, snapshot, template or environment in this workspace |
| 409 | box_limit, name_taken, insufficient_balance, tier_unavailable, workspace_closing, wake_rejected, snapshot_in_progress, box_not_running | The plan's box count, a live box with that name, a balance that cannot run the fleet for four hours, a size the plan refuses, a closing workspace, a wake the balance refuses, a template still being taken, a deadline on a paused box |
| 422 | microvm_unsupported | A snapshot schedule on a fast box |
Operations
| Route | Answer |
|---|---|
GET /api/v1/operations/{id} | 200 {operation}: id, kind (box_delete, snapshot_delete), targetId, status, requestedAt, completedAt, error |
A delete answers with one of these; status runs pending, processing, completed, or failed with error. Finished operations stay readable for 30 days. What a delete keeps and removes is on Data retention.
Snapshots
| Route | Body | Answer |
|---|---|---|
GET /api/v1/snapshots | query transient=1 to include fork copies | 200 {snapshots}, newest first |
POST /api/v1/snapshots | {boxId, name, description?} | 201 {snapshot} with status: "creating"; poll the list until available |
DELETE /api/v1/snapshots/{id} | 202 {snapshot, operation} | |
POST /api/v1/snapshots/{id}/restore | {name?, tier?} | 202 {box}, a new box from the snapshot |
POST /api/v1/snapshots/{id}/template | {name} | 200 {snapshot, previous}; previous is the snapshot that held the name, or null |
DELETE /api/v1/snapshots/{id}/template | 200 {snapshot}, back to a plain snapshot |
A snapshot is {id, name, description, status, kind, templateName, expiresAt, boxId, sourceBoxHostname, tier, region, sizeGb, error, createdAt} (status: creating, available, failed, deleting; kind: manual, template, fork, auto, final). Other fields are internal and may disappear. Names are 1 to 64 characters of letters, digits, ., _ and -, starting and ending with a letter or digit. Kinds, limits and every refusal code are on Snapshots: API; forking and templating a box are under Boxes above.
Environments
| Route | What it does |
|---|---|
GET /api/v1/environments | 200 {environments} |
POST /api/v1/environments | Create; 201 {environment} |
PATCH /api/v1/environments/{id} | Change any field; a config change mints a version; 200 {environment} |
DELETE /api/v1/environments/{id} | 204; boxes keep their version |
POST /api/v1/environments/{id}/default | Make it the default for new boxes |
POST /api/v1/environments/{id}/toggles | The safe-for-third-parties switch and the three credential channels |
POST /api/v1/environments/{id}/vars, DELETE …/vars/{name} | Set or remove one env var |
PUT /api/v1/environments/{id}/files, DELETE …/files?path= | Set or remove one secret file |
POST /api/v1/environments/{id}/repos, DELETE …/repos?repo= | Add or remove one repo |
POST /api/v1/environments/{id}/upgrade | Pin live boxes to the latest version; 200 {upgraded, skipped, latestVersionNo} |
Bodies, the environment object, limits and error codes are on Environments: API.
Secrets
Brokered credentials: the box sees a placeholder, and the broker substitutes the real value only in HTTPS requests to the allowed hosts. What that means for a box is on Connectors and Credentials; these are the routes behind both pages.
| Route | Body | Answer |
|---|---|---|
GET /api/v1/secrets | 200 {secrets}, never a value | |
POST /api/v1/secrets | {name, value, description?, hosts?} | 201 {secret} |
PATCH /api/v1/secrets/{id} | {value?, description?, hosts?} (at least one) | 200 {secret}; a new value keeps the placeholder and stamps lastRotatedAt |
DELETE /api/v1/secrets/{id} | 204; its mounts go with it | |
GET /api/v1/boxes/{id}/secrets | 200 {mounts} | |
POST /api/v1/boxes/{id}/secrets | {secretId, envName} to mount an existing secret, or {name, value, envName, hosts?, description?} to create and mount in one step | 201 {mount} |
DELETE /api/v1/boxes/{id}/secrets/{mountId} | 204 |
A secret is {id, name, description, hosts, placeholder, mountCount, createdAt, updatedAt, lastRotatedAt}; a mount is {id, envName, secret: {id, name, placeholder, hosts}}. name is UPPER_SNAKE_CASE (up to 128 characters), value up to 16 KiB, hosts up to 32 entries, each a hostname or a *. wildcard; envName is a shell identifier.
| HTTP | error | When |
|---|---|---|
| 400 | bad_request, invalid_name, invalid_host, invalid_env_name | The body is off |
| 402 | secret_limit | The workspace's secret count is full |
| 409 | name_taken, env_name_taken, box_restricted | A secret with that name, a mount on that variable, a box that is safe for third parties |
| 503 | secrets_key_unconfigured | The broker's key is not configured on this deployment |
Tunnel routes
GET /api/v1/ssh-meta, POST /api/v1/connect, POST /api/v1/disconnect, POST /api/v1/ssh-cert and PUT /api/v1/device-ssh-key are how prized ssh builds its WireGuard tunnel and SSH certificate. They are the CLI's, not a surface for scripts: to run something on a box, use the edge or prized exec. Two more routes are the CLI's own housekeeping, POST /api/v1/telemetry/cli (its usage batches) and POST /api/v1/advisor/messages (the environment advisor proxy).
Dashboard only
These answer 401 unauthorized to any bearer, a valid one included, so that a leaked token cannot mint more credentials or change who is in the workspace. Use the dashboard.
| Routes | What they are |
|---|---|
POST, GET /api/v1/cli-tokens, DELETE /api/v1/cli-tokens/{id}, POST /api/v1/cli-tokens/revoke-all | CLI tokens |
POST /api/v1/terminal-tickets, POST /api/v1/install-tokens | The browser terminal's ticket; the single-use install link |
GET, POST, DELETE /api/v1/boxes/{id}/mobile-ssh | The phone access password |
PATCH /api/v1/workspace, POST /api/v1/workspace/invite, join, leave, close, reopen, erase, DELETE /api/v1/workspace/members/{userId}, /api/v1/workspace/icon | Workspace settings, membership, and closing |
The edge
GET /api/v1/me gives edge.url; every route below is under $EDGE/v1/box/{box}/, where {box} is the box's name or id, with the same bearer. Each request opens its own tunnel and SSH session to the box, so a revoked token dies on its next request and nothing is ever served from a pool. POST /exec and POST /prompts wake a suspended box (they were asked for work); every other route answers 409 box_not_running instead. Paths on the box are read the way prized cp reads them: relative paths and ~/… are under your home, absolute paths are taken as is, and every answer echoes the real path it touched.
Every successful answer carries ok: true and a type. A failed one is {"ok": false, "error": {"code", "message"}}; the codes are listed once, on Run commands and files: Errors, with the prompt routes' additions on Prompt an agent remotely: Errors. A known path with the wrong method is 405 with Allow; an unknown path is 404 not_found, in the same envelope.
Commands
| Route | Body or query | Answer |
|---|---|---|
POST /exec | {command, cwd?, timeoutSeconds? (1 to 600, default 30), env? (up to 32), detached?} | 200 exec.result: exitCode, signal, stdout, stderr, stdoutTruncated, stderrTruncated, timedOut, startedAt, finishedAt, cwd |
POST /exec with detached: true | the same | 200 exec.started: processId, pid, cwd, startedAt, logPath |
GET /exec | 200 exec.list: processes[] of processId, pid, running, lost, exitCode, signal, startedAt, command | |
GET /exec/{processId} | query tail (bytes; default 16384, max 1048576) | 200 exec.status: the list fields plus finishedAt, cwd, stdout, stderr, stdoutTruncated, stderrTruncated |
DELETE /exec/{processId} | 200 exec.killed: processId, wasRunning, signal (TERM, or KILL when TERM was not enough) |
The command runs under sh -c in your home (or cwd) as your box user; the time limit and the timedOut verdict, output truncation, and the on-box process directory are on Run commands and files.
Files
| Route | Body or query | Answer |
|---|---|---|
GET /files | query path | 200 file.read: path, encoding (utf8 or base64), size, mode, content; over 1 MiB answers 413 and points at /download |
PUT /files | {path, content, encoding? (utf8 or base64), mode? ("0644")} | 200 file.written: path, size |
GET /download | query path | The file as application/octet-stream with Content-Length, or a directory as application/x-tar with X-Prized-Content: directory; X-Prized-Path carries the real path |
PUT /upload | query path, mode?; the raw file as the body | 200 file.uploaded: path, size |
Size caps are on Limits: Commands and files; path rules and error codes on Run commands and files.
Prompts and events
| Route | Body or query | Answer |
|---|---|---|
POST /prompts | {provider (claude or codex), prompt (up to 512 KiB), model?, reasoningEffort? (low, medium, high), cwd?, continue?, queue?, auto?} | 202 prompt.started: run; or 202 prompt.queued: queued, active |
GET /prompts | 200 prompt.list: runs[] newest first (at most 50), active (the run in progress, or null) | |
GET /prompts/{runId} | 200 prompt.status: run with the full prompt, stderr (last 4 KiB), stderrTruncated | |
GET /prompts/{runId}/events | query after (a seq, default 0), limit (1 to 1000, default 200; a page also stops at 1 MiB) | 200 prompt.events: run, events[], next, finished; pass next as the following after |
GET /prompts/{runId}/events?follow=1 | or Accept: text/event-stream | Server-Sent Events until the run ends |
POST /prompts/{runId}/interrupt | 200 prompt.interrupted: id, wasRunning, signal | |
POST /prompts/interrupt | The same, for the run in progress |
A run is {id, provider, model, reasoningEffort, auto, status (running, done, failed, interrupted), exitCode, pid, cwd, startedAt, finishedAt, sessionId, resumedFrom, parent, eventCount, prompt}. An event is {seq, at, type, text, tool: {name, input}, raw}, described on Prompt an agent remotely: JSON and JSON Lines.
The stream form's event names and keep-alive, and approvals, continuity, the queue and the run directory on the box, are on Prompt an agent remotely: The edge API.
Desktop
| Route | Body | Answer |
|---|---|---|
POST /desktop/stop | {mode?} (desktop, the default, or browser) | 200 {ok: true, box, mode}; never wakes a paused box |
This one route predates the box API's envelope: its errors are the bare {"error": "code"} form, with unauthorized (a dcp_ token only; tickets are refused here), not_found, bad_request, box_not_running, box_unreachable, desktop_stop_failed, resolve_failed and control_plane_unavailable.
The desktop stream itself is a dashboard WebSocket authenticated by a terminal ticket; from a script use prized desktop --no-open, which prints a vnc:// address and the password, see Desktop.
Examples
Create a box and wait for it to run:
BOX=$(curl -s -X POST "$API/boxes" -H "$AUTH" -H "Content-Type: application/json" -d '{"name":"ci-42","tier":"flow","ttlMinutes":120}' | jq -r .box.id)
# 202; the row comes back with desiredState running and observedState requested
until [ "$(curl -s "$API/boxes/$BOX" -H "$AUTH" | jq -r .box.observedState)" = running ]; do sleep 5; doneRun a command on it and read the exit code:
curl -s -X POST "$EDGE/v1/box/ci-42/exec" -H "$AUTH" -H "Content-Type: application/json" -d '{"command":"make test","cwd":"app","timeoutSeconds":300}' | jq '{exitCode, timedOut, signal, stderrTruncated}'
# {"exitCode": 0, "timedOut": false, "signal": "", "stderrTruncated": false}Upload a file and download a directory:
curl -s -X PUT "$EDGE/v1/box/ci-42/upload?path=data/train.bin" -H "$AUTH" --data-binary @train.bin
# {"ok": true, "type": "file.uploaded", "path": "/home/you/data/train.bin", "size": 104857600}
curl -s "$EDGE/v1/box/ci-42/download?path=app/dist" -H "$AUTH" -o dist.tarPrompt the agent on an idle box and follow it as it works (a box with a run in progress answers prompt.queued with no run; queue behind it with queue: true, or interrupt it first):
RUN=$(curl -s -X POST "$EDGE/v1/box/ci-42/prompts" -H "$AUTH" -H "Content-Type: application/json" -d '{"provider":"claude","prompt":"fix the failing tests in app/ and run them again","cwd":"app","auto":true}' | jq -r .run.id)
curl -sN "$EDGE/v1/box/ci-42/prompts/$RUN/events" -H "$AUTH" -H "Accept: text/event-stream"
# event: prompt
# data: {"seq":1,"type":"system",...}
# ...
# event: done
# data: {"run":{"id":"pr_…","status":"done",...},"next":41}Snapshot the box, then fork it:
curl -s -X POST "$API/snapshots" -H "$AUTH" -H "Content-Type: application/json" -d "{\"boxId\":\"$BOX\",\"name\":\"pre-upgrade\"}" | jq '{id: .snapshot.id, status: .snapshot.status}'
# {"id": "snp_…", "status": "creating"} poll GET /snapshots until available
curl -s -X POST "$API/boxes/$BOX/fork" -H "$AUTH" -H "Content-Type: application/json" -d '{"name":"ci-42-b"}' | jq '{box: .box.id, state: .box.observedState, copy: .snapshot.id}'
# {"box": "…", "state": "requested", "copy": "snp_…"} the copy deletes itself once the fork runsDelete the box and confirm it is gone:
OP=$(curl -s -X DELETE "$API/boxes/$BOX" -H "$AUTH" | jq -r .operation.id)
# 202 {"box": {...}, "operation": {"id": "dop_…", "kind": "box_delete", "status": "pending", ...}}
until [ "$(curl -s "$API/operations/$OP" -H "$AUTH" | jq -r .operation.status)" = completed ]; do sleep 5; doneNot yet
- No SDK. The routes above with
curlor your language's HTTP client are the whole interface; the CLI's--jsonoutput is the other supported machine interface, see CLI: For agents and scripts. - No OpenAPI document. This page is the reference.
- No webhooks. Poll
GET /api/v1/boxes/{id}orGET /api/v1/operations/{id}; the one push channel is the prompt event stream. - No API keys apart from CLI tokens. A token is a signed-in machine with your full standing in the workspace; there is no read-only or per-box token. Mint one per script and revoke it when the script is retired.
- No idempotency keys, see Conventions.
Something unclear or out of date?
CLI
The Prized CLI (macOS, Linux, and Windows) runs the whole box lifecycle and connects you to your boxes; add --help to any command for full flags. Every non-interactive command also takes --json and prints exactly one JSON object.
Dashboard
Every screen in the signed-in app at prized.dev/dashboard, what each control does, and where a setting lives. The feature pages explain the behaviour; this one says where to click.