API
The control plane is REST with JSON at https://raw.computer/v1. Its single source is the OpenAPI 3.1 document at
/v1/openapi.json: these docs, the SDKs and the CLI are made from it. Where
this page and the document disagree, the document is right.
Authentication
Send a workspace API key in the Authorization header:
Authorization: Bearer rc_live_…
A key is made in the console (API keys) or with POST /v1/api_keys, belongs to one person and acts in one
workspace, as developer (reads and writes) or viewer (reads). A key is never an admin and never acts at org level,
and it never does more than its person may do in its workspace. What only a workspace's admins do (its people,
invitations, name and residency) takes a personal token of an admin. A missing, unknown or revoked key gets
401 unauthorized; a key without the relation an action needs gets 403 forbidden.
Personal tokens
The CLI signs in with a personal token (rc_pat_…) instead. It acts as its person in whichever of
their workspaces a request names, with their relation there (an org's owner administers each of its workspaces), never
more, and never at org level. A request that acts in a workspace names it:
Authorization: Bearer rc_pat_…
Raw-Workspace: ws_01m6xxgde9f2c9q3y3gn9e8zdp
Without Raw-Workspace, such a request is 400 invalid_request; GET /v1/me, GET /v1/workspaces and the
endpoints that take a workspace in their path need none. A workspace key may send Raw-Workspace too, naming its own
workspace; naming another is 403 forbidden. GET /v1/me says which kind a token is (api_key.kind: workspace or
personal).
A personal token is made by the device flow (RFC 8628), with no key:
POST /v1/device/codewith{ "client_name": "raw CLI on build-7" }answersdevice_code,user_code(WDJB-MJHT),verification_uri_complete,expires_in(600) andinterval(5).- The person opens
verification_uri_complete, signed in to the console, checks the code and approves it. POST /v1/device/tokenwith{ "device_code": "…" }, everyintervalseconds, answers400 authorization_pendinguntil then, and then201with the token (secret), once. A refusal is400 access_denied; a code past its ten minutes, unknown or used,400 expired_token.
A token is revoked with DELETE /v1/api_keys/{id}, by itself (raw logout) or by its person in the console.
Versions
Raw-Version: 2026-11-08
Versions are dates. A request without Raw-Version is answered in the version its API key is pinned to: the one
that was current on the day the key was made. Every answer says which version it was given in, in its own
Raw-Version header, and GET /v1/me returns it as raw_version. A version that does not exist is
400 invalid_request.
Additive changes (a new field, a new endpoint) need no version. A breaking change gets a new date, and the server keeps
older versions working through a shim for at least 24 months. A version on its way out will answer with Deprecation
and Sunset headers. Today there is one version, 2026-11-08.
Idempotency
Any POST may carry an Idempotency-Key header: a unique string of 1 to 255 characters; a UUID is fine. It is
optional, and it makes a retry safe.
Idempotency-Key: 6f1c1a52-3a0e-4c35-a7e5-2b0d0e1f7c11
- Keys are scoped to the API key that sends them and kept for 24 hours.
- A request is matched by a hash of its method, path and body. The same request with the same key is answered as the
first one was, with the header
Idempotent-Replayed: true, and nothing runs twice. - A different request with the same key is
409 idempotency_conflict. So is a second request sent while the first is still being answered; retry it in a moment. - Only successful (2xx) answers are kept. Any error, 4xx or 5xx, releases the key, so a retry with the same key
runs again. After a
402 insufficient_funds, top up and retry the very same request. - A replayed
POST /v1/api_keysanswers with"secret": null: the secret is shown once and never stored.
Ids
Ids are typed by prefix, so a log line tells you what it names:
| Prefix | Names | Form |
|---|---|---|
org_ |
an org | org_ + 26 characters |
ws_ |
a workspace | ws_ + 26 characters |
key_ |
an API key | key_ + 26 characters |
usr_ |
a person | usr_ + the person's account id |
inv_ |
an invitation | inv_ + 26 characters |
ent_ |
a ledger entry | ent_ + a number |
sbx_ |
a sandbox | sbx_ + its cell's code + _ + 26 characters |
snp_ |
a snapshot | snp_ + 26 characters |
tpl_ |
a template | tpl_ + 26 characters |
op_, evt_ |
an operation, an event | arrive with the endpoints that return them |
The 26 characters are lower-case Crockford base32 (0-9a-z without i, l, o, u), and they sort in the order the
ids were made. Treat every id as an opaque string: compare it whole, never parse it.
Lists, money and time
- A list is
{ "object": "list", "data": [ … ], "has_more": false }. Lists that page takelimitandstarting_after(the last id of the page you have). - Money is an integer number of micro-dollars in fields ending
_usd_micros:1000000is $1.00. - Times are RFC 3339 in UTC, such as
2026-11-08T17:02:11.000Z; days areYYYY-MM-DDin UTC.
Endpoints
| Method and path | What it answers | Who may |
|---|---|---|
GET /v1/me |
the key's person, the key, the workspace the request acts in and its org, and the version | any key |
GET /v1/workspaces |
the workspaces the key acts in (one, for a workspace key) | any key |
GET /v1/workspaces/{id} |
one workspace | any key of it |
GET /v1/workspaces/{id}/budget |
the workspace's cap, spend this month, what is held, what is left, and can_start |
any key of it |
POST /v1/workspaces/{id} |
renames it (name) or sets where its sandboxes may live (residency): see below |
an admin's token |
GET /v1/members |
the key's workspace's members: its grants, and the org's owners as admin (via) |
any key |
POST /v1/members/{id} |
sets a member's one relation: { "relation": "viewer" | "developer" | "admin" } |
an admin's token |
DELETE /v1/members/{id} |
removes a member: every grant of theirs in the workspace ends | an admin's token |
GET /v1/invitations |
the workspace's invitations nobody accepted yet, oldest first | an admin's token |
POST /v1/invitations |
mails an invitation: { "email", "relation"? } (developer by default), valid seven days |
an admin's token |
DELETE /v1/invitations/{id} |
withdraws a pending invitation; its link stops working | an admin's token |
GET /v1/api_keys |
the keys of the workspace that the key's person made; never a secret | any key |
POST /v1/api_keys |
a new key of the same person, with its secret, once | developer keys |
DELETE /v1/api_keys/{id} |
revokes a key of the same person (a key may revoke itself) | developer keys |
GET /v1/usage |
usage per UTC day, class and size; from and to (default: the last 30 days, at most 366) |
any key |
GET /v1/wallet |
the org's balance and ledger entries, newest first; limit 1–100 (20), starting_after |
keys of an org owner or billing member |
GET /v1/sandboxes |
the workspace's sandboxes, newest first; limit 1–200 (50), starting_after |
any key |
POST /v1/sandboxes |
starts a sandbox: see below | developer keys |
GET /v1/sandboxes/{id} |
one sandbox, as its cell last reported it | any key |
POST /v1/sandboxes/{id}/stop |
stops it, keeping its disk: see Acting on a sandbox | developer keys |
POST /v1/sandboxes/{id}/start |
starts it again from its disk | developer keys |
POST /v1/sandboxes/{id}/fork |
a new sandbox from this one's disk and memory; body { "name"?, "on_full"? } |
developer keys |
DELETE /v1/sandboxes/{id} |
deletes it and its disk | developer keys |
GET /v1/snapshots |
the workspace's snapshots, newest first; sandbox, limit 1–200 (50), starting_after |
any key |
POST /v1/snapshots |
snapshots a sandbox: see Snapshots and templates | developer keys |
GET /v1/snapshots/{id} |
one snapshot | any key |
DELETE /v1/snapshots/{id} |
deletes it, freeing its unique bytes | developer keys |
GET /v1/templates |
the templates the workspace may start from: its own and our public ones, by name | any key |
GET /v1/templates/{id} |
one template | any key |
POST /v1/device/code |
starts a sign-in of the CLI (device flow): see Personal tokens | anyone, no key |
POST /v1/device/token |
the sign-in's personal token, once it is approved | anyone, with its device_code |
Every endpoint that acts in "the workspace" acts in the key's own, or in the one a personal token names with
Raw-Workspace. "An admin's token" is a personal token whose person is an admin of the workspace, or an owner of its
org; a workspace key asking is 403 forbidden, with raw login as its next step.
A workspace's people and settings
An admin does with one call what the console's Members and Settings pages do:
POST /v1/invitations
Authorization: Bearer rc_pat_…
Raw-Workspace: ws_01m6xxgde9f2c9q3y3gn9e8zdp
Idempotency-Key: invite-ada
{ "email": "ada@example.com", "relation": "developer" }
{
"object": "invitation",
"id": "inv_01m6xxk2w9c4t7r5q3y8vzd6na",
"email": "ada@example.com",
"relation": "developer",
"created_at": "2026-11-09T09:12:00.000Z",
"expires_at": "2026-11-16T09:12:00.000Z"
}
The person gets a link by email and accepts it signed in to the console, as themselves; they are then a member, as
GET /v1/members lists them (usr_…). POST /v1/members/{id} makes the relation given their one relation in the
workspace, and DELETE /v1/members/{id} removes them (their keys of the workspace stop working). A workspace sends at
most a few invitations a minute, the console's and the API's together (Limits); past it,
429 rate_limited says when to send again.
POST /v1/workspaces/{id} takes name (1 to 100 characters; its code stays), residency (any, eu, or null to
follow the org's default), or both, and answers the workspace; every workspace answer carries its residency.
An org's owners administer every workspace of it without a grant (via: "org_owner"): changing or removing one is
403 forbidden, with the console as the next step.
Only in the console
Some things are done only by a person signed in to the console, because they act at org level, or need the person themselves, and no key or token ever does (a token is never more than a workspace's member):
| What | Where | Why not the API |
|---|---|---|
| Make a member an owner of the org | Members | ownership is the org's; a token never acts at org level |
| Rename the org | Settings | the org's |
| Create a workspace | Settings | an owner of the org makes it, at org level |
| Set a monthly cap, the workspace's or the org's | Billing | it is money: no key or token writes money |
| Accept an invitation | the link in the email | the person invited accepts, signed in as themselves |
| Delete an account | Settings | only the person themselves, signed in |
Any key reads the result: GET /v1/workspaces/{id}/budget answers the caps and can_start, GET /v1/members the
owners.
Money an agent can read
GET /v1/wallet is the org's money, so it answers only a key whose person is an owner or a billing member of the org
(read only: no key writes money). Every other key gets 403 forbidden, with the budget as its next step:
{
"object": "budget",
"workspace": "ws_01m6xxgde9f2c9q3y3gn9e8zdp",
"monthly_cap_usd_micros": null,
"spent_usd_micros": 0,
"held_usd_micros": 0,
"remaining_usd_micros": null,
"can_start": false
}
can_start says whether the org's wallet and the caps leave room to start a sandbox now; when it is false, a create
answers 402 insufficient_funds. A cap of null means the workspace has none of its own (the org's may still hold).
Creating a sandbox
{ "class": "std", "size": "4x8", "name": "build-42", "on_full": "queue" }
class is std or perf; size is one the class sells (Limits); name is optional;
template (optional) is a tpl_… to start from, as GET /v1/templates lists it; on_full is queue (the default)
or fail. The checks run in this order:
- The body and the size:
400 invalid_request(for example8x32 is not a size of the perf class.). - The template, when one is named:
404 not_foundfor one the workspace may not start from. - The key's relation:
403 forbiddenfor aviewerkey. - Capacity. When no cell serves the class in the workspace's residency,
503 capacity_unavailable, whateveron_fullsays, with noRetry-After. This is every create during early access. When cells serve it but are full,202with the sandboxqueued, or with"on_full": "fail",503 capacity_fullwithRetry-After. - Money:
402 insufficient_fundswhencan_startis false.
Once cells are open, a create that starts at once answers 201 with the sandbox; one that waits answers 202:
{
"object": "sandbox",
"id": "sbx_ny1_01m6xxh3k2q8v7c4t9r5wdz6ya",
"name": "build-42",
"class": "std",
"size": "4x8",
"state": "queued",
"created_at": "2026-11-08T17:02:11.000Z",
"queue": { "position": 3, "eta": "2026-11-08T17:04:00.000Z" }
}
A sandbox's state is queued, starting, running, stopping, stopped or failed, as its cell last reported
it, or unknown before its cell has reported.
Acting on a sandbox
A sandbox id is sbx_, the code of the cell that holds it, _, and 26 characters, as GET /v1/sandboxes lists it.
Each of these reads the directory the cells write first: an id of another form, one in another workspace, a deleted
sandbox or one never made is 404 not_found, with GET /v1/sandboxes as the next step. A viewer key may read a
sandbox but not act on it (403 forbidden).
Stopping, starting, forking and deleting are done by the sandbox's cell. During early access no cell takes them
through the API, so a sandbox the directory holds is answered 503 capacity_unavailable, never with a state made up
on our side; the directory holds none yet, so today every id is 404. Once cells are open, stop and start answer 202
with the sandbox stopping or starting, a fork 201 or 202 like a create (paid and queued the same way), and a
delete 200 with { "object": "sandbox", "id", "deleted": true }.
Snapshots and templates
Snapshots and templates are read from the same directory the cells write, so they are listed the moment a cell reports them. During early access no cell does, and the lists are empty.
{
"object": "snapshot",
"id": "snp_01m6xxj8w4c2t7q9r3v5n6pz1k",
"name": "before-upgrade",
"sandbox": "sbx_ny1_01m6xxh3k2q8v7c4t9r5wdz6ya",
"unique_size_bytes": 1342177280,
"expires_at": null,
"created_at": "2026-11-08T17:20:00.000Z"
}
GET /v1/snapshots?sandbox=sbx_…lists one sandbox's snapshots, a deleted sandbox's too: a snapshot outlives its sandbox. Asandboxnot of a sandbox's form is400 invalid_request; one the workspace does not hold,404.unique_size_bytesis what only this snapshot holds: what deleting it frees, and what its storage is billed on.expires_atis when its cell will delete it, ornullwhen it is kept until someone does.- A template is
{ "object": "template", "id", "name", "workspace", "cpu_baseline", "created_at" }.workspaceis the workspace whose template it is, ornullfor one of our public templates, which every workspace may start from.cpu_baseline(such aszen5-v1) is the CPU its memory image was made on.GET /v1/templatesis one page (has_moreisfalse). - An id of another form, of another workspace, or of a deleted snapshot is
404 not_found, with the list as its next step; a public template is found from every workspace.
Taking a snapshot (POST /v1/snapshots with { "sandbox": "sbx_…", "name"? }), deleting one and starting a sandbox
from a template are done by cells, like acting on a sandbox: during early access one the
directory holds is answered 503 capacity_unavailable, anything else 404, and a viewer key 403. Once cells are
open, a snapshot answers 202 with the snapshot and a delete 200 with { "object": "snapshot", "id", "deleted": true }.
Coming with the first cells
Long operations (?wait=ready long-polls, GET /v1/operations/{id}); events (GET /v1/events?stream=1 as server-sent
events, and signed webhooks). Each appears in the OpenAPI document when it is answered, not before.
Errors
Every error has one shape:
{
"error": {
"code": "insufficient_funds",
"message": "The wallet or a budget cap cannot pay for a new sandbox in this workspace.",
"next_step": {
"message": "Top up the wallet or raise the cap, then retry the same request with the same Idempotency-Key.",
"url": "https://raw.computer/app/billing",
"api": "GET /v1/workspaces/ws_01m6xxgde9f2c9q3y3gn9e8zdp/budget"
},
"docs": "https://raw.computer/docs/errors#insufficient_funds"
}
}
codeis stable; branch on it, never onmessage.messagesays what happened, in one sentence.next_step.messageis always there: one plain sentence a person or a model can act on.next_step.url,next_step.api(asMETHOD /v1/path) andnext_step.cli(arawcommand, such asraw login) are optional, machine-usable hints.retry_at(RFC 3339) appears beside aRetry-Afterheader when a later call can succeed: onrate_limited(Limits › Rate limits) andcapacity_full.docslinks to the code on the Errors page, which lists all ten codes.