Prized docs

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

HostWhat lives thereA box is addressed byError envelope
https://api.prized.dev/api/v1The control plane: boxes, snapshots, environments, secrets, deletion operations, your accountIts 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/meIts 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.

Terminal
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/stop included, takes a dcp_ 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 answers 426 cli_update_required with minVersion. 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 as 400 bad_request. A 204 has no body.
  • Ids. Control-plane routes take the box's id (a uuid) in the path, never its name; GET /api/v1/boxes maps names to ids. Snapshots are snp_…, environments env_…, secrets sec_…, deletion operations dop_…. An id that is unknown, malformed, or belongs to another workspace is 404 not_found, with nothing written.
  • 202 means the reconciler is on it. Creating, waking, pausing, resizing, moving, forking, restoring and deleting a box answer 202 with the row as written: desiredState has moved and observedState follows over the next seconds to minutes. Poll GET /api/v1/boxes/{id} until observedState is what you asked for, or GET /api/v1/operations/{id} for a delete. Setting writes (PATCH, templates, secrets, environments) answer 200 or 201 because nothing is left to converge.
  • Errors. The control plane answers {"error": "code"}, plus message on the refusals that have a reason and extra fields where a client needs numbers (insufficient_balance carries balanceMicros, requiredMicros, burnMicrosPerHour; snapshot_limit carries limit; box_not_running carries state). 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/boxes retried after a lost response creates a second box; give the box a name and the retry answers 409 name_taken instead, then GET /api/v1/boxes finds 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 a 202 no-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, and 429 customer_session_limit on the CLI's tunnel sessions. The edge admits 8 concurrent requests and 20 per second per token and per box, and answers 429 rate_limited with Retry-After; see Limits: Commands and files.
  • Times and money. Timestamps are RFC 3339 UTC. Balances are in micro-dollars (1000000 is $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 --json prints. Fields and routes are added under /v1 without notice; ignore fields you do not know. Nothing beyond that is promised yet.

Account

RouteAnswer
GET /api/v1/mecustomer (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

RouteBodyAnswer
GET /api/v1/boxes200 {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}/wake202 {box}, desiredState: "running"
POST /api/v1/boxes/{id}/suspend202 {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}/metricsquery 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-atquery 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.

HTTPerrorWhen
400bad_request, invalid_name, invalid_template, invalid_tier, invalid_env, invalid_auto_pause, invalid_pause_at, invalid_auto_snapshot, environment_too_largeThe body is off; message names the window or the rule where there is one
400resize_rejected, move_rejectedA refusal on a box you own (mid-transition, a fifth move in a day, a region the fleet cannot launch in); message says why
403restore_tier_not_allowedThe size is off your plan or its disk cannot hold the snapshot
404not_foundNo such box, snapshot, template or environment in this workspace
409box_limit, name_taken, insufficient_balance, tier_unavailable, workspace_closing, wake_rejected, snapshot_in_progress, box_not_runningThe 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
422microvm_unsupportedA snapshot schedule on a fast box

Operations

RouteAnswer
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

RouteBodyAnswer
GET /api/v1/snapshotsquery transient=1 to include fork copies200 {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}/template200 {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

RouteWhat it does
GET /api/v1/environments200 {environments}
POST /api/v1/environmentsCreate; 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}/defaultMake it the default for new boxes
POST /api/v1/environments/{id}/togglesThe 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}/upgradePin 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.

RouteBodyAnswer
GET /api/v1/secrets200 {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}/secrets200 {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 step201 {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.

HTTPerrorWhen
400bad_request, invalid_name, invalid_host, invalid_env_nameThe body is off
402secret_limitThe workspace's secret count is full
409name_taken, env_name_taken, box_restrictedA secret with that name, a mount on that variable, a box that is safe for third parties
503secrets_key_unconfiguredThe 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.

RoutesWhat they are
POST, GET /api/v1/cli-tokens, DELETE /api/v1/cli-tokens/{id}, POST /api/v1/cli-tokens/revoke-allCLI tokens
POST /api/v1/terminal-tickets, POST /api/v1/install-tokensThe browser terminal's ticket; the single-use install link
GET, POST, DELETE /api/v1/boxes/{id}/mobile-sshThe 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/iconWorkspace 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

RouteBody or queryAnswer
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: truethe same200 exec.started: processId, pid, cwd, startedAt, logPath
GET /exec200 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

RouteBody or queryAnswer
GET /filesquery path200 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 /downloadquery pathThe 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 /uploadquery path, mode?; the raw file as the body200 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

RouteBody or queryAnswer
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 /prompts200 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}/eventsquery 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=1or Accept: text/event-streamServer-Sent Events until the run ends
POST /prompts/{runId}/interrupt200 prompt.interrupted: id, wasRunning, signal
POST /prompts/interruptThe 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

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

Terminal
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; done

Run a command on it and read the exit code:

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

Terminal
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.tar

Prompt 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):

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

Terminal
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 runs

Delete the box and confirm it is gone:

Terminal
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; done

Not yet

  • No SDK. The routes above with curl or your language's HTTP client are the whole interface; the CLI's --json output 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} or GET /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?

On this page