# Introduction raw.computer rents full Linux virtual machines (we call them **sandboxes**) to AI agents, CI systems and people. Each one is a real computer with its own kernel: systemd, Docker, nested virtualization, a persistent disk, snapshots and forks. You pay per second while it runs, and nothing to start it. > **Early access.** The API, accounts, keys and the wallet work today. No [cell](/docs/concepts#cells) (a group of our hosts that runs sandboxes) is open > to customers yet, so creating a sandbox answers `503 capacity_unavailable` > ([Limits › Early access](/docs/limits#early-access)). Pages say which parts arrive with the first cells. ## Three ways in | You are | Start with | | ---------- | ------------------------------------------------------------------------------------------------------------------- | | An agent | [`/llms.txt`](https://raw.computer/llms.txt), then any page of these docs as Markdown: add `.md` to its URL. | | A program | The HTTP API at `https://raw.computer/v1`, described by [`/v1/openapi.json`](https://raw.computer/v1/openapi.json). | | A person | The [console](https://raw.computer/app): sign up, create an API key, top up. | | A terminal | The `raw` CLI is [coming](/docs/cli). Until then, `curl` does everything. | Everything a person can do in the console, an agent can do with one API call, and every error says what to do next ([Errors](/docs/errors)). ## What makes it different - **Full VMs, not containers.** Your code gets a kernel, an init system and real devices. Docker runs inside without tricks. - **No start fees.** Create, start, resume and fork cost nothing. Billing is per second of running time. - **Queue, don't fail.** When a cell is full, a start waits in line and tells you its position and an ETA. Callers that would rather fail say so and get the ETA in the error. - **Built for agents.** Typed ids, dated API versions pinned per key, safe retries with `Idempotency-Key`, a budget any key can read before it starts work, and errors that name the next step. ## Pages - [Quickstart](/docs/quickstart): from sign-up to your first API call. - [Concepts](/docs/concepts): orgs, workspaces, sandboxes, snapshots, templates, sizes, classes, the queue, residency. - [API](/docs/api): authentication, versions, idempotency, long operations, the error shape. - [Errors](/docs/errors): every error code and what to do about it. - [CLI](/docs/cli): the command-line tool we are building. - [Pricing](/docs/pricing): every price, and how it is billed. - [Limits](/docs/limits): sizes, lifetimes and rates. --- # Quickstart Four steps, a few minutes, with the [CLI](/docs/cli). Rather use `curl`? [With curl and an API key](#with-curl-and-an-api-key) does the same over the [API](/docs/api). ## 1. Sign up Create an account at [raw.computer/sign-up](https://raw.computer/sign-up) and verify your email. You get an **org** (it holds the wallet) with one **workspace** called `default` (it holds keys and sandboxes). See [Concepts](/docs/concepts) for how the two relate. ## 2. Install the CLI and sign in ```sh curl -fsSL https://raw.computer/install.sh | sh raw login ``` `raw login` shows a code and opens the console, where you check the code and approve it. The CLI keeps a personal token that acts as you, in your workspaces, and keeps itself up to date. ## 3. Check who you are ```sh raw whoami ``` ```text Person ada (usr_DEMJ4NCDUxm2h7orD6T6WkB16m74Tvx8) Token raw CLI on laptop (key_01m6xxge1kf83t1tk6pss3awgm, personal; stored by raw login) Workspace default · Default (ws_01m6xxgde9f2c9q3y3gn9e8zdp) as admin Org ada (org_01m6xxgde9eaer8hxj9q8kx58d) API https://raw.computer (version 2026-11-08) ``` Add `--json` to any command for the API's own JSON (here, the answer of `GET /v1/me`). ## 4. Create a sandbox ```sh raw sandboxes create --class std --size 4x8 ``` During early access no cell is open to customers, so this is the answer you get today: ```text raw: No cell serves std yet: raw.computer is in early access. [capacity_unavailable] next: Read the early-access note; sandbox creates open when the first cell does. see: https://raw.computer/docs/limits#early-access docs: https://raw.computer/docs/errors#capacity_unavailable ``` It is honest on purpose: a request that no cell can serve is refused rather than queued, so nothing waits for capacity that does not exist, and the CLI exits `1`. Once cells open, the same command starts a sandbox, or queues it with its position and ETA ([Concepts › The queue](/docs/concepts#the-queue)). ## With curl and an API key In the console, open **API keys**, give the key a name and choose **Create key**. The key starts with `rc_live_` and is shown once; only its hash is kept. A key acts in one workspace, as `developer` (reads and writes, the default) or `viewer` (reads), and never at org level. Keep it in an environment variable: ```sh export RAW_API_KEY="rc_live_…" ``` Check who you are: ```sh curl https://raw.computer/v1/me \ -H "Authorization: Bearer $RAW_API_KEY" \ -H "Raw-Version: 2026-11-08" ``` ```json { "object": "me", "user": { "object": "user", "id": "usr_DEMJ4NCDUxm2h7orD6T6WkB16m74Tvx8", "name": "ada", "email": "ada@example.com" }, "api_key": { "object": "api_key", "id": "key_01m6xxge1kf83t1tk6pss3awgm", "name": "laptop", "kind": "workspace", "relation": "developer" }, "workspace": { "object": "workspace", "id": "ws_01m6xxgde9f2c9q3y3gn9e8zdp", "code": "default", "name": "Default", "org": { "object": "org", "id": "org_01m6xxgde9eaer8hxj9q8kx58d", "name": "ada" }, "relation": "developer" }, "raw_version": "2026-11-08" } ``` `Raw-Version` pins the API version your code was written against. Leave it out and the request is answered in the version your key is pinned to: the one that was current on the day you made the key. See [API › Versions](/docs/api#versions). Create a sandbox: ```sh curl https://raw.computer/v1/sandboxes \ -H "Authorization: Bearer $RAW_API_KEY" \ -H "Raw-Version: 2026-11-08" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "class": "std", "size": "4x8" }' ``` ```http HTTP/1.1 503 Service Unavailable Content-Type: application/json; charset=utf-8 Raw-Version: 2026-11-08 { "error": { "code": "capacity_unavailable", "message": "No cell serves std yet: raw.computer is in early access.", "next_step": { "message": "Read the early-access note; sandbox creates open when the first cell does.", "url": "https://raw.computer/docs/limits#early-access" }, "docs": "https://raw.computer/docs/errors#capacity_unavailable" } } ``` An error never holds on to its `Idempotency-Key`, so the same request can be sent again later. Once cells open, the same call answers `201` with the sandbox starting, or `202` with it `queued` and its position and ETA. The CLI takes the same key: `RAW_API_KEY=rc_live_… raw sandboxes list`. ## Next - [CLI](/docs/cli): every command, `--json`, and using it in CI. - [Agents](/docs/agents): `raw agent setup` gives Claude Code, Codex and Cursor the skill and the MCP tools. - [Concepts](/docs/concepts): orgs, workspaces, sandboxes and the queue. - [API](/docs/api): every endpoint, idempotency and money. - [Errors](/docs/errors): every code and its next step. --- # Concepts ## Orgs and workspaces An **org** (`org_…`) is the tenant. It owns money and trust: the wallet, the ledger, the default residency. Org owners administer every workspace of the org; billing members see the org's money but not its work. A **workspace** (`ws_…`) belongs to one org and is where work happens: members, API keys, sandboxes, budgets and usage. A member holds one relation in it: `admin`, `developer` or `viewer`, each including the next. Every org starts with a workspace called `default`. ## API keys An **API key** (`key_…`, secret `rc_live_…`) belongs to one person and acts in one workspace, as `developer` (reads and writes) or `viewer` (reads). A key is never an admin, never acts at org level, and does nothing once its person leaves the workspace. The secret is shown once; only its hash is kept. Personal tokens, which act as their person in every workspace that person belongs to, arrive with the [CLI](/docs/cli). ## Sandboxes A **sandbox** (`sbx_…`) is a full virtual machine: its own Linux kernel, systemd, Docker, nested virtualization when you ask for it, a persistent disk, and a network. Its id carries the code of the [cell](#cells) it lives in. A sandbox is `queued`, `starting`, `running`, `stopping`, `stopped` or `failed`, as its cell last reported it (`unknown` before its cell has reported). You pay compute only while it runs, per second, and storage for its disk while it is stopped ([Pricing](/docs/pricing)). Creating, starting, resuming and forking cost nothing. ## Snapshots and forks A **snapshot** (`snp_…`) captures a sandbox's disk, and, for a running sandbox, its memory. A **fork** is a new sandbox started from a snapshot or from a running sandbox; it keeps the parent's files and state and diverges from there. Storage is billed on **unique bytes**: blocks shared between a sandbox, its snapshots and its forks are counted once. Forking a sandbox is `POST /v1/sandboxes/{id}/fork` ([API › Acting on a sandbox](/docs/api#acting-on-a-sandbox)). A workspace's snapshots are listed by `GET /v1/snapshots` (`raw snapshots list`, and under **Machines** in the console) as soon as the cell that holds them reports them; taking and deleting one are its cell's to do, answered once cells are open ([API › Snapshots and templates](/docs/api#snapshots-and-templates)). A snapshot outlives its sandbox. ## Templates A **template** (`tpl_…`) is a disk image to start sandboxes from: one of ours, or one your workspace built. Templates are stored once per CPU baseline, so a sandbox started from one can run on any host of its pool. `GET /v1/templates` (`raw templates list`, and **Machines** in the console) lists the workspace's own and our public ones (`workspace` is `null`) as the cells report them; `POST /v1/sandboxes` with `"template": "tpl_…"` starts from one once cells are open. ## Sizes A size is `x`. Each class sells the sizes marked for it; asking a class for another is `400 invalid_request`. | Size | vCPU | Memory | Disk included while running | `std` | `perf` | | --- | --- | --- | --- | --- | --- | | `2x4` | 2 | 4 GiB | 20 GiB | yes | yes | | `2x8` | 2 | 8 GiB | 20 GiB | yes | — | | `4x8` | 4 | 8 GiB | 40 GiB | yes | yes | | `4x16` | 4 | 16 GiB | 40 GiB | yes | — | | `8x16` | 8 | 16 GiB | 80 GiB | yes | yes | | `8x32` | 8 | 32 GiB | 80 GiB | yes | — | | `16x32` | 16 | 32 GiB | 160 GiB | yes | yes | | `16x64` | 16 | 64 GiB | 160 GiB | yes | — | | `32x64` | 32 | 64 GiB | 320 GiB | yes | yes | | `32x128` | 32 | 128 GiB | 320 GiB | yes | — | ## Classes | Class | For | CPU | | ------ | ------------------------------------- | ------------------------------------------------- | | `std` | agents and dev boxes | shared vCPU, packed by measured workload profiles | | `perf` | CI and anything that waits on the CPU | dedicated high-clock cores | Prices for every size in each class are on [Pricing](/docs/pricing). ## Cells A **cell** is a group of our hosts in one region that runs sandboxes. Each sandbox lives in exactly one cell, and its id names that cell: `sbx__…`. The regions a workspace may use follow its [residency](#residency). - When every cell that serves a start's class and region is full, the start waits in line with an ETA ([The queue](#the-queue)); with `"on_full": "fail"` it answers `503 capacity_full` instead. - `503 capacity_unavailable` means no cell serves that class and region at all. During early access that is true of every class and region. - EU residency means EU cells only. ## The queue A start never fails just because a cell is busy. When cells exist but are full, the default is to wait in line: the answer is `202` with the sandbox `queued`, its place in the queue and when it is expected to start. ```json { "object": "sandbox", "id": "sbx_ny1_01m6xxh3k2q8v7c4t9r5wdz6ya", "name": null, "class": "std", "size": "4x8", "state": "queued", "created_at": "2026-11-08T17:02:11.000Z", "queue": { "position": 3, "eta": "2026-11-08T17:04:00.000Z" } } ``` Queued time is not billed. A caller that would rather fail says so: ```json { "class": "perf", "size": "8x16", "on_full": "fail" } ``` and gets `503 capacity_full`, with `Retry-After` in seconds and the expected time in the message. When **no** cell serves the requested class in the workspace's residency, the answer is `503 capacity_unavailable`, whatever `on_full` says, with no `Retry-After`. Nothing would ever drain that queue, so we don't pretend it would. That is the answer during early access ([Limits › Early access](/docs/limits#early-access)). ## Residency An org has a default **residency**: `any` (the default) or `eu`. A workspace may set its own. With `eu`, the workspace's sandboxes, disks, snapshots and templates are placed only in EU [cells](#cells), and a request that would place them elsewhere is refused. EU placement carries a multiplier on compute ([Pricing](/docs/pricing)). --- # 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`](https://raw.computer/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: ```http 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](#personal-tokens) 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](/docs/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: ```http 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](https://www.rfc-editor.org/rfc/rfc8628)), 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 ```http 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. ```http 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](#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](#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](#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: ```http POST /v1/invitations Authorization: Bearer rc_pat_… Raw-Workspace: ws_01m6xxgde9f2c9q3y3gn9e8zdp Idempotency-Key: invite-ada { "email": "ada@example.com", "relation": "developer" } ``` ```json { "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](/docs/limits#rate-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: ```json { "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 ```json { "class": "std", "size": "4x8", "name": "build-42", "on_full": "queue" } ``` `class` is `std` or `perf`; `size` is one the class sells ([Limits](/docs/limits#sizes)); `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`: ```json { "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. ```json { "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](#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: ```json { "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`](/docs/cli) 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](/docs/limits#rate-limits)) and `capacity_full`. - `docs` links to the code on the [Errors](/docs/errors) page, which lists all ten codes. --- # Errors Every error answers with the same shape ([API › Errors](/docs/api#errors)). Branch on `code`; follow `next_step.message`; use `next_step.url`, `next_step.api` and `next_step.cli` when they are there. An error never holds on to an `Idempotency-Key`: after fixing the cause, retry the same request with the same key. | Code | HTTP | In short | | ------------------------------------------------- | ---- | ---------------------------------------------------- | | [`invalid_request`](#invalid_request) | 400 | the request is malformed or a value is wrong | | [`authorization_pending`](#authorization_pending) | 400 | a CLI sign-in nobody has approved yet | | [`access_denied`](#access_denied) | 400 | the person refused a CLI sign-in | | [`expired_token`](#expired_token) | 400 | a CLI sign-in's code expired or was used | | [`unauthorized`](#unauthorized) | 401 | no key, or a key we don't know | | [`insufficient_funds`](#insufficient_funds) | 402 | the wallet or a cap cannot pay for this | | [`forbidden`](#forbidden) | 403 | the key may not do this | | [`not_found`](#not_found) | 404 | no such thing in this workspace, or no such endpoint | | [`idempotency_conflict`](#idempotency_conflict) | 409 | an `Idempotency-Key` was reused for another request | | [`rate_limited`](#rate_limited) | 429 | too many requests; wait `Retry-After` | | [`internal_error`](#internal_error) | 500 | something failed on our side | | [`capacity_full`](#capacity_full) | 503 | cells are full and you asked not to queue | | [`capacity_unavailable`](#capacity_unavailable) | 503 | no cell serves this class and residency at all | ## `invalid_request` **400.** The body is not valid JSON, a field is missing or out of range (a class that does not exist, a size the class does not sell, a `limit` too large), `Raw-Version` names a version that does not exist, `Idempotency-Key` is not 1 to 255 characters, `Raw-Workspace` is not a workspace id, or a personal token names no workspace for a request that acts in one. `message` names the field and what it should be. Fix the request; sending it unchanged gets the same answer. ## `authorization_pending` **400.** The answer to polling `POST /v1/device/token` while nobody has approved the sign-in yet (RFC 8628). Keep polling, no faster than every `interval` seconds of the sign-in, until it is approved, refused or expired. The [CLI](/docs/cli#sign-in) does this for you. ## `access_denied` **400.** The person refused this sign-in on the console's page. The code is spent; start a new sign-in (`raw login`) if it was refused by mistake. ## `expired_token` **400.** The device code is unknown, past its ten minutes, or was used already (a sign-in gives its token once). Start a new sign-in: `POST /v1/device/code`, or `raw login`. ## `unauthorized` **401.** The `Authorization` header is missing or malformed, or holds a key or token that does not exist or was revoked. Send `Authorization: Bearer` with a live API key (`rc_live_…`; `next_step.url` is the console's **API keys** page) or the CLI's personal token (`rc_pat_…`; `next_step.cli` is `raw login`). ## `insufficient_funds` **402.** The org's wallet, or a budget cap, cannot pay for a new sandbox: the workspace's budget says `can_start: false`. `next_step.url` is the console's billing page and `next_step.api` the budget (`GET /v1/workspaces/{id}/budget`). Top up or raise the cap, then retry the same request with the same `Idempotency-Key`. ## `forbidden` **403.** The key is valid, but this needs more than it holds: a `viewer` key cannot create sandboxes or keys, and the wallet answers only keys of an org owner or billing member (any key can read its workspace's budget instead). A workspace's people, invitations, name and residency are its admins': a workspace key is never an admin, so it is told to sign in to the [CLI](/docs/cli) as one (`next_step.cli` is `raw login`), and a personal token of a member below admin to ask one (`raw members list`). Keys act only in their own workspace (naming another with `Raw-Workspace` is refused), personal tokens only where their person holds a relation, and neither ever at org level: an org's owners, its name and its money are changed in the console, which `next_step.url` names ([API › Only in the console](/docs/api#only-in-the-console)). Use a key with the relation this needs, made by a person who holds it. ## `not_found` **404.** Nothing with that id exists in the workspace the request acts in, or no endpoint answers that method and path (then `next_step.url` is the OpenAPI document). Ids are typed (`ws_`, `key_`, `sbx_`, …); check that the id is complete and belongs to the workspace the request acts in. ## `idempotency_conflict` **409.** The `Idempotency-Key` was used in the last 24 hours, by the same API key, for a different request (another method, path or body). Send the original request to get its answer, or use a new key for new work. The same code answers a request whose key is still being answered by an earlier one; retry it in a moment. ## `rate_limited` **429.** Too many requests in a short time: more than an API key may send to `/v1`, or more sign-in, sign-up, password-reset, verification or CLI sign-in attempts than one address may make ([Limits › Rate limits](/docs/limits#rate-limits)). `Retry-After` gives the seconds to wait and `retry_at` the time (RFC 3339), so nothing parses `message`. Retry the same request then, with the same `Idempotency-Key`. ## `internal_error` **500.** Something failed on our side, not in your request. Nothing was kept under the `Idempotency-Key`, so retry the same request with the same key; if it keeps failing, tell us. ## `capacity_full` **503.** Cells that serve the class and residency exist, but they are full, and the request said `"on_full": "fail"`. `Retry-After` gives the seconds until room is expected and `retry_at` the time (RFC 3339), so nothing parses `message`. Retry then, or leave out `on_full` to wait in the queue instead ([Concepts › The queue](/docs/concepts#the-queue)). ## `capacity_unavailable` **503.** No cell serves the requested class in the workspace's residency, so the request cannot be queued: nothing would ever drain it. There is no `Retry-After`. This is the answer to every sandbox create during early access, whatever `on_full` says; `next_step.url` is the [early-access note](/docs/limits#early-access). --- # CLI `raw` is one static binary for macOS, Linux and Windows. It does in one command what a person does in the console, and it answers either in words or, with `--json`, in the API's own JSON. It keeps itself current: once a day it looks for a newer release and swaps itself in the background, so the next run is the new one. ## Install ```sh curl -fsSL https://raw.computer/install.sh | sh ``` The script picks the build for your OS and architecture (macOS or Linux, amd64 or arm64), checks its sha256 against the release, and installs it as `~/.local/bin/raw`, a directory you own, so the CLI can update itself there. Set `RAW_INSTALL_DIR` to install elsewhere. On Windows, download `https://raw.computer/cli//raw-windows-amd64.exe`, where `` is the one [`/cli/latest.json`](https://raw.computer/cli/latest.json) names. ```sh raw version ``` ```text raw 0.1.0 (2c99fd7ffe65) darwin/arm64 ``` ## Sign in ```sh raw login ``` `raw login` prints a code and opens the console's **Sign in the raw CLI** page (pass `--no-browser` to only print it). Signed in to the console, check that the page shows the code your terminal does, and choose **Approve**. The CLI gets a **personal token** (`rc_pat_…`) and keeps it in `~/.config/raw/config.json`, readable only by you (mode `0600`). ```text To sign in, open this page and confirm the code WDJB-MJHT: https://raw.computer/app/device?code=WDJB-MJHT Waiting for approval (the code expires in 10 minutes)… Signed in as ada . Acting in default · Default (org ada) as admin; change it with: raw workspaces use ``` A personal token acts as you, in whichever of your workspaces a command names, with your relation there and never more. It never acts at org level, so it cannot rename an org or change its money. Each sign-in is listed in the console under **API keys › Signed-in CLIs**, where you can sign one out. `raw logout` revokes the stored token and forgets it. In CI and other places with no browser, set an environment variable instead of signing in: | Variable | Holds | | --------------- | ---------------------------------------------------------------------- | | `RAW_TOKEN` | a personal token (`rc_pat_…`); name its workspace with `RAW_WORKSPACE` | | `RAW_API_KEY` | an API key of one workspace (`rc_live_…`, from **API keys**) | | `RAW_WORKSPACE` | the workspace to act in: its id (`ws_…`) or its code | `RAW_TOKEN` wins over `RAW_API_KEY`, and both win over the stored token. A token from the environment acts in the workspace the environment names, never in one chosen for the stored token. ## Workspaces Every command takes `--workspace ` (an id, `ws_…`, or a code such as `ci`). Without it, a command acts in the workspace chosen last: ```sh raw workspaces list raw workspaces use ci ``` ```text CODE NAME ORG RELATION ID * default Default ada admin ws_01m6xxgde9f2c9q3y3gn9e8zdp ci CI ada admin ws_01m6xxh0yq5b8c0k4m2v6t9rza ``` `raw login` chooses one for you: the one chosen before, else one you administer. An API key acts in its own workspace only. ## Commands | Command | What it does | With `--json`, prints | | -------------------------------------------- | ----------------------------------------------------- | -------------------------------- | | `raw login [--no-browser]` | sign in through the browser | `{ "object": "login", … }` | | `raw logout` | revoke the stored token and forget it | `{ "object": "logout", … }` | | `raw whoami` | the person, token, workspace and org in use | `GET /v1/me` | | `raw workspaces list` | your workspaces; `*` marks the one in use | `GET /v1/workspaces` | | `raw workspaces use ` | choose the workspace commands act in | the workspace | | `raw workspaces rename ` | rename the workspace (an admin; its code stays) | `POST /v1/workspaces/{id}` | | `raw workspaces set-residency ` | `any`, `eu`, or `default` (the org's), an admin | `POST /v1/workspaces/{id}` | | `raw keys list` | the workspace's API keys you may see | `GET /v1/api_keys` | | `raw keys create [--relation viewer]` | an API key; its secret is shown once | `POST /v1/api_keys` | | `raw keys revoke ` | revoke an API key | `DELETE /v1/api_keys/{id}` | | `raw members list` | the workspace's members, the org's owners among them | `GET /v1/members` | | `raw members invite [--relation]` | mail an invitation (`developer` by default), an admin | `POST /v1/invitations` | | `raw members set ` | make `viewer`, `developer` or `admin` their relation | `POST /v1/members/{id}` | | `raw members remove ` | remove them from the workspace, an admin | `DELETE /v1/members/{id}` | | `raw invitations list` | the invitations nobody accepted yet, an admin | `GET /v1/invitations` | | `raw invitations withdraw ` | withdraw one; its link stops working | `DELETE /v1/invitations/{id}` | | `raw sandboxes create [--class] [--size]` | a sandbox (`std`, `4x8` by default) | `POST /v1/sandboxes` | | `raw sandboxes list` | the workspace's sandboxes, newest first | `GET /v1/sandboxes` | | `raw sandboxes get ` | one sandbox | `GET /v1/sandboxes/{id}` | | `raw sandboxes stop ` / `start ` | stop it (only its disk is billed), start it again | `POST …/stop`, `POST …/start` | | `raw sandboxes fork ` | a new sandbox from its disk and memory now | `POST …/fork` | | `raw sandboxes delete ` | delete it and its disk | `DELETE /v1/sandboxes/{id}` | | `raw snapshots list [--sandbox sbx]` | the workspace's snapshots, newest first | `GET /v1/snapshots` | | `raw snapshots get ` | one snapshot | `GET /v1/snapshots/{id}` | | `raw snapshots create [--name]` | snapshot a sandbox's disk, and memory if it runs | `POST /v1/snapshots` | | `raw snapshots delete ` | delete a snapshot, freeing its unique bytes | `DELETE /v1/snapshots/{id}` | | `raw templates list` | the templates to start from: the workspace's and ours | `GET /v1/templates` | | `raw templates get ` | one template | `GET /v1/templates/{id}` | | `raw usage [--from day] [--to day]` | usage per day, class and size | `GET /v1/usage` | | `raw wallet` | the org's balance and entries (owners and billing) | `GET /v1/wallet` | | `raw budget` | what the workspace may spend; whether a start can pay | `GET /v1/workspaces/{id}/budget` | | `raw api [--data json]` | any endpoint, as the CLI's token | the body the API answered | | `raw mcp` | the commands as MCP tools, for an agent's harness | MCP on stdin and stdout | | `raw agent setup [--remove]` | skill and `raw mcp` into Claude Code, Codex, Cursor | `{ "object": "agent_setup", … }` | | `raw version` | this build, and a newer release if one is out | `{ "object": "version", … }` | | `raw update` | update now (it happens by itself once a day) | `{ "object": "update", … }` | Every command has `--help`, and `raw help ` is the same. Singular names work too (`raw sandbox ls`). A `` is an email or an id (`usr_…`), and an `` an email or an id (`inv_…`), as the lists show them. What only an admin does needs the token of `raw login` as an admin of the workspace (or an owner of its org): an API key is never an admin, so the CLI with `RAW_API_KEY` is refused with `raw login` as its next step. What acts at org level (an org's owners and name, a new workspace, the monthly caps, accepting an invitation) is the console's ([API › Only in the console](/docs/api#only-in-the-console)). ### Examples ```sh raw keys create ci --relation developer # the secret is printed once; store it raw sandboxes create --class std --size 4x8 --name build-42 raw sandboxes create --size 8x16 --on-full fail # fail at once instead of queueing when cells are full raw sandboxes create --template tpl_01m6xxj9a2b3c4d5e6f7g8h9jk # start from a template (raw templates list) raw snapshots list --sandbox sbx_ny1_01m6xxh3k2q8v7c4t9r5wdz6ya raw members invite ada@example.com --relation admin raw members set ada@example.com developer raw invitations withdraw ada@example.com raw workspaces set-residency eu raw usage --from 2026-11-01 --to 2026-11-08 raw api GET /v1/me raw api POST /v1/api_keys --data '{"name":"deploy","relation":"viewer"}' echo '{"name":"from-stdin"}' | raw api POST /v1/api_keys --data - ``` Every create sends an `Idempotency-Key`, and a request that got no answer is sent again with the same key, so a dropped connection never makes two of anything. Pass `--idempotency-key ` to `create`, `stop`, `start`, `fork`, `raw snapshots create`, `members invite` and the other commands that send a `POST` to keep a retry across runs safe as well: the same command run again is answered as the first was ([API › Idempotency](/docs/api#idempotency)). ## Output Without `--json`, a command writes what it did in a few lines, or a table. With `--json`, **stdout holds exactly one JSON document**: the API's answer as it came (for `raw sandboxes create`, the answer of `POST /v1/sandboxes`), or the error, in the API's [error shape](/docs/errors). Anything meant for a person (the sign-in code, notes) goes to stderr. ```sh raw sandboxes list --json | jq -r '.data[].id' ``` The exit status says how it went: `0` done, `1` refused or failed, `2` a command line that cannot run (an unknown command or flag, a missing argument). ## Errors An error says what happened, then its next step, as the API gives it, with the command to run where one helps: ```text $ raw sandboxes create --class std --size 4x8 raw: No cell serves std yet: raw.computer is in early access. [capacity_unavailable] next: Read the early-access note; sandbox creates open when the first cell does. see: https://raw.computer/docs/limits#early-access docs: https://raw.computer/docs/errors#capacity_unavailable ``` That is every sandbox create today: during early access no cell is open to customers ([Limits](/docs/limits#early-access)). The CLI says so plainly and exits `1`; it never pretends a machine started. The API's codes are on the [Errors](/docs/errors) page. A few the CLI makes itself, in the same shape: | Code | When | | ----------------- | ----------------------------------------------------------------------------- | | `unauthorized` | no token: not signed in, and `RAW_TOKEN` and `RAW_API_KEY` are not set | | `invalid_request` | a command line that cannot run (exit `2`) | | `unreachable` | the API did not answer (the network, or a wrong `RAW_API_URL`) | | `interrupted` | stopped (Ctrl-C) before the API answered | | `local_error` | something on this machine: a file it cannot write, a build that cannot update | ## Updates Once a day, a run of `raw` starts a copy of itself in the background that reads [`/cli/latest.json`](https://raw.computer/cli/latest.json), downloads the build for its OS and architecture when it is newer, checks its sha256, and renames it over the binary. The run that started it does not wait, and the next run is the new version. `raw update` does the same now. Nothing is replaced unless the whole file arrived and its sha256 is the release's. The first run of a release whose skill changed writes the new skill where `raw agent setup` put the old one ([Agents](/docs/agents#the-skill)). It does not update itself in CI (where `CI` is set: install the newest with the script instead), in a development build, or when `RAW_NO_UPDATE=1` is set. If the binary is in a directory you cannot write, `raw update` says so; install it again with the script. ## For agents `raw agent setup` writes the [raw-computer skill](/docs/agents#the-skill) and the MCP server `raw mcp` into Claude Code, Codex and Cursor, so an agent there has the commands as tools and knows how to use them ([Agents](/docs/agents)). Calling the CLI itself: - Use `--json` and branch on the exit status and `error.code`, never on a message. - Name the workspace with `--workspace` (or `RAW_WORKSPACE`) so a command never depends on a choice stored earlier. - `raw api` reaches any endpoint of the [API](/docs/api) with the CLI's token and version. - Every error's `next_step.cli`, when there is one, is a `raw` command that helps. ## Environment | Variable | What it does | | ----------------- | ------------------------------------------------------------------- | | `RAW_TOKEN` | a personal token to use instead of the stored one | | `RAW_API_KEY` | an API key to use instead of the stored token | | `RAW_WORKSPACE` | the workspace to act in (id or code) | | `RAW_API_URL` | the API's origin (default `https://raw.computer`), or `--api ` | | `RAW_CONFIG_DIR` | where the CLI keeps its files (default `~/.config/raw`) | | `RAW_NO_UPDATE` | `1`: never update by itself | | `RAW_INSTALL_DIR` | for `install.sh`: where to install (default `~/.local/bin`) | --- # SDKs raw.computer has an SDK for TypeScript and one for Python. Both are generated from the API's [OpenAPI document](https://raw.computer/v1/openapi.json), so every operation of the [HTTP API](/docs/api) is a typed method of theirs, named by its operationId, and both behave the same way: the version they were made from on every request, an `Idempotency-Key` on every POST, retries as `Retry-After` says, and one typed error. > **Early access:** the packages are not on npm or PyPI yet. They will be `npm install rawcomputer` and > `pip install rawcomputer`. Until then, use the [HTTP API](/docs/api) or the [CLI](/docs/cli), which do the same. ## TypeScript For Node 20 or later, Bun, Deno, Workers and browsers: anywhere `fetch` runs. ```ts import { Raw, RawError } from 'rawcomputer'; const raw = new Raw(); // RAW_API_KEY, or RAW_TOKEN with RAW_WORKSPACE const me = await raw.getMe(); const acting = me.workspace ? `${me.workspace.code} (${me.workspace.id})` : 'no workspace'; console.log( `me: ${me.user.email} with a ${me.api_key.kind} key, in ${acting}, API version ${me.raw_version}` ); for (const workspace of (await raw.listWorkspaces()).data) console.log( `workspace: ${workspace.code} · ${workspace.name} (${workspace.id}) as ${workspace.relation}` ); console.log(`sandboxes: ${(await raw.listSandboxes()).data.length}`); try { await raw.createSandbox({ body: { class: 'std', size: '4x8' } }); } catch (e) { if (!(e instanceof RawError)) throw e; console.log(`createSandbox: RawError ${e.status} ${e.code}: ${e.message}`); console.log(` next step: ${e.next_step.message} ${e.next_step.url ?? ''}`.trimEnd()); console.log(` docs: ${e.docs}`); } ``` ```text me: ada@example.com with a workspace key, in default (ws_01m6zbey4kf5db2f5qbn7fwppy), API version 2026-11-08 workspace: default · Default (ws_01m6zbey4kf5db2f5qbn7fwppy) as developer sandboxes: 0 createSandbox: RawError 503 capacity_unavailable: No cell serves std yet: raw.computer is in early access. next step: Read the early-access note; sandbox creates open when the first cell does. https://raw.computer/docs/limits#early-access docs: https://raw.computer/docs/errors#capacity_unavailable ``` A method takes one object with the parts its operation has: `body`, `path` (`raw.getSandbox({ path: { id } })`), `query` (`raw.listSandboxes({ query: { limit: 10 } })`) and `headers`. Its answer and its body are typed from the document; every type is exported (`import type { Sandbox } from 'rawcomputer'`). ## Python For Python 3.11 or later. `AsyncRaw` is the same client with every method awaited. ```python from rawcomputer import Raw, RawError from rawcomputer.models import SandboxCreate raw = Raw() # RAW_API_KEY, or RAW_TOKEN with RAW_WORKSPACE me = raw.get_me() acting = f"{me.workspace.code} ({me.workspace.id})" if me.workspace else "no workspace" print(f"me: {me.user.email} with a {me.api_key.kind} key, in {acting}, API version {me.raw_version}") for workspace in raw.list_workspaces().data: print(f"workspace: {workspace.code} · {workspace.name} ({workspace.id}) as {workspace.relation}") print(f"sandboxes: {len(raw.list_sandboxes().data)}") try: raw.create_sandbox(body=SandboxCreate(class_="std", size="4x8")) except RawError as e: print(f"create_sandbox: RawError {e.status} {e.code}: {e.message}") print(f" next step: {e.next_step.message} {e.next_step.url or ''}".rstrip()) print(f" docs: {e.docs}") ``` ```text me: ada@example.com with a personal key, in default (ws_01m6zbey4kf5db2f5qbn7fwppy), API version 2026-11-08 workspace: default · Default (ws_01m6zbey4kf5db2f5qbn7fwppy) as admin sandboxes: 0 create_sandbox: RawError 503 capacity_unavailable: No cell serves std yet: raw.computer is in early access. next step: Read the early-access note; sandbox creates open when the first cell does. https://raw.computer/docs/limits#early-access docs: https://raw.computer/docs/errors#capacity_unavailable ``` Methods are the operationIds in snake_case (`get_me`, `list_workspaces`, `create_sandbox`). A path parameter comes first (`raw.get_sandbox("sbx_…")`), everything else by keyword (`body=`, `limit=`, `raw_workspace=`). Request and answer classes are in `rawcomputer.models`; a field the API may leave out is `UNSET`. ## Credentials | What | TypeScript | Python | | ----------------------------------------------- | --------------------------------------- | ------------------------------ | | A workspace API key (`rc_live_…`) | `new Raw({ apiKey })` | `Raw(api_key)` | | A personal token (`rc_pat_…`) and its workspace | `new Raw({ apiKey: token, workspace })` | `Raw(token, workspace="ws_…")` | | Another origin | `new Raw({ baseUrl })` | `Raw(base_url=…)` | Left out, they are read from the environment as the [CLI](/docs/cli#sign-in) reads it: `RAW_TOKEN`, else `RAW_API_KEY`; `RAW_WORKSPACE`; `RAW_API_URL`. Make a key in the console under **API keys**; a personal token is what `raw login` keeps. A token acts as its person in the workspace it names (sent as `Raw-Workspace`) and in none without it; one call can name another (`headers: { 'Raw-Workspace': … }` in TypeScript, `raw_workspace=` in Python). A credential of neither kind is refused before any request is sent. ## Versions Every request names the API version the SDK was made from in `Raw-Version` (`RAW_VERSION`, today `2026-11-08`), so an answer has the shape the SDK reads whatever version the key is pinned to. A value the API adds later (a new state, a new error code) is read as it came, never refused. ## Idempotency and retries - Every POST carries an `Idempotency-Key`, made by the SDK unless the call names its own, and each retry sends the same one: a POST that reached the API is answered as the first time and never done twice ([Idempotency](/docs/api#idempotency)). - 429 and a 5xx with `Retry-After` are sent again after it, when the wait is at most `maxRetryWait` / `max_retry_wait` (60 seconds); a longer wait is yours to choose, and the error's `retry_at` says when. - 500, 502 and 504 without `Retry-After`, and a request that got no answer, are sent again after 0.5, 1, 2… seconds, when they are safe to repeat: a GET, or a POST with its key. - At most `maxRetries` / `max_retries` (3) times. Anything else is yours at once: a 4xx, and a [503 `capacity_unavailable`](/docs/errors#capacity_unavailable), which no wait mends. ## Errors Every answer that is not a success throws (TypeScript) or raises (Python) a `RawError`: | Field | Holds | | ----------- | ---------------------------------------------------------------------------------------------- | | `status` | the HTTP status | | `code` | the stable code to branch on: one of [Errors](/docs/errors) | | `message` | what happened, in one sentence | | `next_step` | what to do: `message`, and where one helps, `url`, `api` (`"POST /v1/api_keys"`) and `cli` | | `docs` | the code on the Errors page | | `retry_at` | when a retry is expected to succeed, on `capacity_full` and `rate_limited`; else null / `None` | A request that got no answer at all, after its retries, throws what `fetch` or httpx2 threw. --- # Agents An agent reaches raw.computer three ways, and they answer alike: the [`raw` CLI](/docs/cli) with `--json`, the [HTTP API](/docs/api), and the **raw MCP server** (`raw mcp`), whose tools are the CLI's commands. The **raw-computer skill** tells the agent how to use them: signing in, the budget, creating sandboxes, Idempotency-Key and `next_step`. One command puts both into the harnesses on your machine: ```sh curl -fsSL https://raw.computer/install.sh | sh raw login raw agent setup ``` ```text Claude Code skill ~/.claude/skills/raw-computer/SKILL.md written mcp ~/.claude.json mcpServers.raw written Codex skill ~/.agents/skills/raw-computer/SKILL.md written mcp ~/.codex/config.toml [mcp_servers.raw] written Cursor not found ~/.cursor The MCP server runs: /home/ada/.local/bin/raw mcp Restart a harness that is running to load them. Take them out with: raw agent setup --remove ``` During early access every sandbox create answers [`capacity_unavailable`](/docs/errors#capacity_unavailable) ([Limits](/docs/limits#early-access)), and the skill says so, so an agent tells you rather than retrying. Running commands in a sandbox, files and SSH are not there yet; the skill and the tools offer only what the API answers today. ## The skill `raw-computer` is an [Agent Skill](https://agentskills.io): one `SKILL.md` with a name and a description, which a harness loads when a task needs it. It covers: - signing in: `raw login` for a person at a browser, an API key in `RAW_API_KEY` where there is none; - the three ways in (MCP tools, the CLI with `--json`, the API) and that they are the same commands; - reading the workspace's budget first, and starting nothing when `can_start` is `false`; - creating a sandbox, the queue, and what `capacity_unavailable` means during early access; - sending an `Idempotency-Key` and reusing it on a retry; - branching on `error.code` and doing what `next_step` says. Install it with `raw agent setup` (below), or into any harness the [skills CLI](https://skills.sh) knows: ```sh npx skills add https://raw.computer ``` The site publishes it for discovery at two paths: | Path | What it holds | | ------------------------------------------------- | ------------------------------------------------------------------------------ | | `/.well-known/agent-skills/index.json` | the index (discovery v0.2.0): each skill's URL and the sha256 of its file | | `/.well-known/skills/index.json` | the same skills in the index's earlier form (v0.1: a list of files) | | `/.well-known/agent-skills/raw-computer/SKILL.md` | the skill itself; `/.well-known/skills/raw-computer/SKILL.md` is the same file | There is one copy of each skill. The CLI carries the same file inside its binary, so when the CLI updates itself to a release whose skill changed, its next run writes the new one where `raw agent setup` put the old. ## `raw agent setup` ```sh raw agent setup # every harness found raw agent setup --harness codex # only this one, found or not raw agent setup --remove # take out what it wrote ``` It looks in your home directory for each harness and writes, at user level so every project has them: | Harness | Found by | The skill | The MCP server | | ----------- | ------------------------------- | ---------------------------------------- | --------------------------------------------------- | | Claude Code | `~/.claude` or `~/.claude.json` | `~/.claude/skills/raw-computer/SKILL.md` | `mcpServers.raw` in `~/.claude.json` | | Codex | `~/.codex` (`CODEX_HOME`) | `~/.agents/skills/raw-computer/SKILL.md` | a marked `[mcp_servers.raw]` block in `config.toml` | | Cursor | `~/.cursor` | `~/.agents/skills/raw-computer/SKILL.md` | `mcpServers.raw` in `~/.cursor/mcp.json` | Codex and Cursor both read `~/.agents/skills`, so they share one copy (Cursor would otherwise list the skill twice). `CLAUDE_CONFIG_DIR` moves Claude Code's paths, as it does for Claude Code. The MCP server's command is the full path of the `raw` binary that ran setup, so a harness started from an app, without your shell's `PATH`, still finds it. - **Run again, it changes nothing** that is right already: each line says `unchanged`. - **It touches only its own entry.** Every other key of a JSON file stays, in its order; `config.toml` stays byte for byte outside the block between `# >>> raw.computer` and `# <<< raw.computer <<<`. An `env` you add to the raw entry is kept. - **What is yours stays yours:** a server named `raw` that runs something else, or a `[mcp_servers.raw]` you wrote by hand, is left alone (`kept`), and `--remove` leaves it too. - **`--remove`** deletes the skill and the entry it wrote, and nothing else; a skill another harness still set up reads is kept. With `--json`, stdout holds one document: ```json { "object": "agent_setup", "action": "setup", "mcp_command": ["/home/ada/.local/bin/raw", "mcp"], "harnesses": [ { "id": "claude-code", "name": "Claude Code", "found": true, "skills": [{ "path": "/home/ada/.claude/skills/raw-computer/SKILL.md", "status": "written" }], "mcp": { "path": "/home/ada/.claude.json", "status": "written" } }, { "id": "cursor", "name": "Cursor", "found": false, "skills": [], "mcp": null } ] } ``` A status is `written`, `unchanged`, `removed`, `absent` (nothing to remove), `kept` (not ours, left alone, with a `note`) or `failed` (with a `note` saying why; the command then exits `1`). No harness found is not an error: it says so, and how else to get the skill. ## The MCP server `raw mcp` is a local MCP server on stdin and stdout, started by the harness. To add it by hand instead of with `raw agent setup`: ```sh claude mcp add --scope user raw -- raw mcp # Claude Code codex mcp add raw -- raw mcp # Codex ``` ```json { "mcpServers": { "raw": { "command": "raw", "args": ["mcp"] } } } ``` (the last for Cursor's `~/.cursor/mcp.json`, or any harness that reads that shape). It acts with the token `raw login` stored, or with `RAW_API_KEY` (an API key) or `RAW_TOKEN` and `RAW_WORKSPACE` in its environment, as the CLI does. `raw mcp --workspace ci` sets the workspace a tool acts in when a call names none, and `--api` or `RAW_API_URL` another origin. It reads the stored token at every call, so `raw login` while the harness runs is enough. | Tool | Is | Returns | | -------------------------- | ------------------------------------------ | -------------------------------- | | `whoami` | `raw whoami` | `GET /v1/me` | | `workspaces_list` | `raw workspaces list` | `GET /v1/workspaces` | | `members_list` | `raw members list` | `GET /v1/members` | | `invitations_list` | `raw invitations list` | `GET /v1/invitations` | | `workspaces_rename` | `raw workspaces rename ` | `POST /v1/workspaces/{id}` | | `workspaces_set-residency` | `raw workspaces set-residency ` | `POST /v1/workspaces/{id}` | | `sandboxes_list` | `raw sandboxes list` | `GET /v1/sandboxes` | | `sandboxes_get` | `raw sandboxes get ` | `GET /v1/sandboxes/{id}` | | `sandboxes_create` | `raw sandboxes create` | `POST /v1/sandboxes` | | `sandboxes_stop` | `raw sandboxes stop ` | `POST /v1/sandboxes/{id}/stop` | | `sandboxes_start` | `raw sandboxes start ` | `POST /v1/sandboxes/{id}/start` | | `sandboxes_fork` | `raw sandboxes fork ` | `POST /v1/sandboxes/{id}/fork` | | `sandboxes_delete` | `raw sandboxes delete ` | `DELETE /v1/sandboxes/{id}` | | `snapshots_list` | `raw snapshots list` | `GET /v1/snapshots` | | `snapshots_get` | `raw snapshots get ` | `GET /v1/snapshots/{id}` | | `snapshots_create` | `raw snapshots create ` | `POST /v1/snapshots` | | `snapshots_delete` | `raw snapshots delete ` | `DELETE /v1/snapshots/{id}` | | `templates_list` | `raw templates list` | `GET /v1/templates` | | `templates_get` | `raw templates get