Skip to content
Docs / API Pages

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:

  1. POST /v1/device/code with { "client_name": "raw CLI on build-7" } answers device_code, user_code (WDJB-MJHT), verification_uri_complete, expires_in (600) and interval (5).
  2. The person opens verification_uri_complete, signed in to the console, checks the code and approves it.
  3. POST /v1/device/token with { "device_code": "…" }, every interval seconds, answers 400 authorization_pending until then, and then 201 with the token (secret), once. A refusal is 400 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_keys answers 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 take limit and starting_after (the last id of the page you have).
  • Money is an integer number of micro-dollars in fields ending _usd_micros: 1000000 is $1.00.
  • Times are RFC 3339 in UTC, such as 2026-11-08T17:02:11.000Z; days are YYYY-MM-DD in 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:

  1. The body and the size: 400 invalid_request (for example 8x32 is not a size of the perf class.).
  2. The template, when one is named: 404 not_found for one the workspace may not start from.
  3. The key's relation: 403 forbidden for a viewer key.
  4. Capacity. When no cell serves the class in the workspace's residency, 503 capacity_unavailable, whatever on_full says, with no Retry-After. This is every create during early access. When cells serve it but are full, 202 with the sandbox queued, or with "on_full": "fail", 503 capacity_full with Retry-After.
  5. Money: 402 insufficient_funds when can_start is 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. A sandbox not of a sandbox's form is 400 invalid_request; one the workspace does not hold, 404.
  • unique_size_bytes is what only this snapshot holds: what deleting it frees, and what its storage is billed on. expires_at is when its cell will delete it, or null when it is kept until someone does.
  • A template is { "object": "template", "id", "name", "workspace", "cpu_baseline", "created_at" }. workspace is the workspace whose template it is, or null for one of our public templates, which every workspace may start from. cpu_baseline (such as zen5-v1) is the CPU its memory image was made on. GET /v1/templates is one page (has_more is false).
  • 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"
	}
}
  • code is stable; branch on it, never on message.
  • message says what happened, in one sentence.
  • next_step.message is always there: one plain sentence a person or a model can act on.
  • next_step.url, next_step.api (as METHOD /v1/path) and next_step.cli (a raw command, such as raw login) are optional, machine-usable hints.
  • retry_at (RFC 3339) appears beside a Retry-After header when a later call can succeed: on rate_limited (Limits › Rate limits) and capacity_full.
  • docs links to the code on the Errors page, which lists all ten codes.