{"openapi":"3.1.0","info":{"title":"Mandala Computer API","version":"1.0.0","description":"Full cloud computers your agents can see, control, and operate.\n\nThe REST resource operations documented here carry an API key as a bearer token; OPTIONS is public path metadata. Keys are created in Settings, through explicit browser approval (device login), or with `POST /api-keys` by a key its holder allowed to manage keys (Settings → Credentials → API keys → the key’s menu → Allow managing keys) and whose holder is a member or owner, and are shown once. Human device login uses separate authentication bootstrap endpoints outside this bearer resource API: see https://app.mandala.computer/docs/device-login for the JSON protocol.\n\n```\ncurl -H \"Authorization: Bearer com_...\" https://app.mandala.computer/api/v1/computers\n```\n\nA key acts as one account. A key issued against a workspace is confined to it and sees only the computers in that workspace, which is what makes a leaked CI key a bounded problem.\n\nThe access tokens the hosted MCP server at `https://app.mandala.computer/mcp` signs in with are not API keys: sent here directly, one is refused with `401` (`reason: \"invalid\"`). Use an API key.\n\nHTTP conventions: every public GET route also accepts HEAD with no response body. HEAD retains the GET role and scope checks. Ordinary finite reads compute GET metadata; file and legacy exec-PID HEAD use native metadata checks, never resume a guest or consume output. Build-event HEAD opens no stream or held stream slot. Artifact/output HEAD verifies retained metadata and file safety without reading chunks. Body-derived headers or failures detectable only by reading content may be unavailable. Range is ignored on HEAD.\n\nOPTIONS is public, bodyless path metadata. It returns only that public path’s Allow methods, including HEAD for GET; unknown, unsafe and daemon-only paths remain 404. An authenticated unsupported method on a known public path returns 405 with the same path-specific Allow, after authority and rate checks. For /computers: GET, HEAD, POST, OPTIONS.\n\nListings: there is no single paging scheme, and most listings have none: they answer everything in one response and take no page parameters. Three page. GET /operations and GET /computers/{id}/activities take an opaque cursor and answer next_cursor; GET /credits takes an integer before and answers next_before. Send next_cursor or next_before back until it is null. The activities changes_cursor is not a page cursor and is never null: it is a feed position, read on from with changes=1. GET /computers/{id}/signals is a feed rather than a listing: send the cursor it answered as since. A few listings are capped rather than paged, and say so: GET /webhooks/{id}/deliveries answers the newest hundred, and GET /computers/{id}/files/list marks a cut-short directory truncated: true.\n\nApplication responses carry a server-generated X-Request-ID. Finite HTTP error objects and application SSE error frames also carry request_id; OpenAI errors retain their nested error object and streaming [DONE] terminator. Caller X-Request-ID is not accepted as the correlation identity. Successful binary/SSE content is unchanged. HEAD errors carry correlation only in the header. Non-JSON, malformed, aborted, oversized (over 1 MiB), or stalled (over 2 seconds of error-body reading) upstream errors use a small correlated envelope with the original status. Infrastructure failures outside the application may have a different shape. The desktop, terminal and browser WebSocket handshakes are an exception: a refusal from the platform’s edge, before the handshake reaches the computer’s host, carries no X-Request-ID and no request_id.\n\nIdempotency-Key is honoured only on the routes that document it (the lifecycle operations). Every other route ignores the header: it is neither validated nor deduplicated, so a retried POST /webhooks, POST /api-keys, POST /secrets, POST /workspaces, POST /ssh-keys or snapshot capture does the work again — a second webhook, API key, secret, workspace or snapshot, or a refusal because the first one already holds that name or SSH key. List before retrying a create whose answer you did not see.\n\nA path with a trailing slash is redirected (308) to the same path without it before the API sees the request, so that redirect carries no X-Request-ID; send paths without one.\n\nPlatform API-key 401 reasons are missing (no Authorization attempt), invalid (malformed or unknown credential), and revoked (retained evidence of revocation). WWW-Authenticate is Bearer for missing and Bearer error=\"invalid_token\" for invalid/revoked. Revocation evidence contains hashes only, retained for at most 90 days and the newest 100,000 hashes globally; older, pre-upgrade or evicted evidence means invalid. A fresh revoked request cannot recover the former holder’s identity. Known suspension or lost membership remains an authority 403; provider 401 errors do not mean the platform API key is invalid.\n\nThe computer’s events_url is a WebSocket capability: use the exact returned URL with its embedded controlling desktop credential. Do not replace its origin with the REST base URL. A Bearer API key alone receives uniform JSON 400 guidance naming events_url; it does not authenticate the socket."},"servers":[{"url":"https://app.mandala.computer/api/v1"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Account","description":"Effective plan ceilings and advisory remaining quota."},{"name":"Templates","description":"What a computer can be built from, and the sizes it can be built at."},{"name":"Builds","description":"Compiling a template document into an image of your own."},{"name":"Computers","description":"Creating machines, and their lifecycle."},{"name":"Moves","description":"When a resize needs a bigger host than the one the computer is on."},{"name":"Control","description":"Driving the desktop: what is on screen, and acting on it."},{"name":"Retained results","description":"Explicit immutable output prefixes, independent of volatile execution handles."},{"name":"Platform signals","description":"Passive, ephemeral daemon observations with bounded replay."},{"name":"Activities","description":"Safe retained summaries of selected API actions."},{"name":"Agents","description":"One call that drives a computer until the task is done, the threads that continue it, and runs in the background."},{"name":"Files","description":"Moving files in and out of the guest."},{"name":"Snapshots","description":"Capturing a computer, and building from a capture."},{"name":"Schedules","description":"Automatic snapshots: when they are taken, and how long they are kept."},{"name":"Usage","description":"What the account has used, and how much of it has settled for billing."},{"name":"Webhooks","description":"Being told when something happens, instead of asking."},{"name":"SSH","description":"Your SSH keys, and which computers accept them."},{"name":"Public URLs","description":"A port inside a computer, served on its own HTTPS address. A deployment can switch the feature off, for every account or for all but some: making or changing one is then a 404 with code PUBLIC_URLS_DISABLED, while listing (enabled: false) and deleting still work."},{"name":"Secrets","description":"Credentials the account stores once and binds to computers. Values go in and never come out."},{"name":"API keys","description":"Who a key is, and minting and revoking keys from a key the dashboard allowed to."},{"name":"Operations","description":"One id to wait on for any lifecycle call, whether it finished before answering or runs on after."},{"name":"Workspaces","description":"The partitions of the account a key can be confined to, and the people who reach them."}],"paths":{"/account":{"get":{"summary":"Read account quota","description":"Read the effective plan, account pool ceilings, per-computer maxima and current consumption before choosing a create or resize. Viewer or stronger; workspace-scoped keys receive whole-account aggregates without resource, workspace, member or billing identities. An authorized holder may read a suspended account to understand its quota. Suspended holders and revoked credentials remain refused.\n\nREAD complete BEFORE USING NUMBERS. An incomplete computer or snapshot collection returns 200 with every usage and remaining field for that group null. The other group and verified caps remain usable. Complete empty inventories are real zero; zero ceilings on a no-plan account do not erase retained usage. Overages remain visible in usage even though remaining is clamped to zero.\n\nSnapshot headroom is against indexed stored bytes, including pending/deleting entries and each physical copy during handover. Capture placeholders have zero indexed bytes; private in-flight capture reservations are not exposed. This is not an available reservation for a new capture.\n\nAdvisory and uncached: concurrent changes can make the observation stale immediately, and observed_at is no consistency token. Creates count the effective template or snapshot disk floor. Resizes replace configured CPU/disk and allow reductions or unchanged overages after downgrade. A stopped resize consumes no running RAM; later start checks active or reserved count, CPU and RAM separately. Host fit and move consent remain separate. Existing mutation admission and 402 messages are unchanged. This read never stops, starts or resizes a computer.\n\nAdmission counts two things this read leaves out: creates and clones whose outcome was never heard, and the reservations held for seeds your own templates boot. See `advisory` on the response.\n\nRequires the `viewer` role or higher.","operationId":"getAccount","tags":["Account"],"x-rate-cost":10,"responses":{"200":{"description":"Verified plan ceilings and independently complete or unknown quota totals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountQuota"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"scope":"account","advisory":true,"observed_at":"2026-09-16T12:00:00.000Z","plan":{"id":"solo","label":"Solo"},"limits":{"max_computers":4,"vcpu_pool":4,"ram_pool_mb":8192,"disk_pool_gb":80,"snapshot_storage_bytes":171798691840},"per_computer":{"max_vcpu":4,"max_ram_mb":8192,"max_disk_gb":40},"capabilities":{"windows":false},"complete":{"computers":true,"snapshots":true},"usage":{"kept_computers":1,"configured_vcpu":2,"configured_disk_gb":20,"running_or_reserved_computers":1,"running_or_reserved_vcpu":2,"running_or_reserved_ram_mb":2048,"snapshot_storage_bytes":0},"remaining":{"kept_computers":3,"configured_vcpu":2,"configured_disk_gb":60,"running_or_reserved_ram_mb":6144,"snapshot_storage_bytes":171798691840}}},"incomplete-inventory":{"summary":"Successful quota observation with unavailable computer totals.","value":{"scope":"account","advisory":true,"observed_at":"2026-09-16T12:00:00.000Z","plan":{"id":"solo","label":"Solo"},"limits":{"max_computers":4,"vcpu_pool":4,"ram_pool_mb":8192,"disk_pool_gb":80,"snapshot_storage_bytes":171798691840},"per_computer":{"max_vcpu":4,"max_ram_mb":8192,"max_disk_gb":40},"capabilities":{"windows":false},"complete":{"computers":false,"snapshots":true},"usage":{"kept_computers":null,"configured_vcpu":null,"configured_disk_gb":null,"running_or_reserved_computers":null,"running_or_reserved_vcpu":null,"running_or_reserved_ram_mb":null,"snapshot_storage_bytes":0},"remaining":{"kept_computers":null,"configured_vcpu":null,"configured_disk_gb":null,"running_or_reserved_ram_mb":null,"snapshot_storage_bytes":171798691840}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/account' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.account.read()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.account.read();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read account quota (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetAccount","tags":["Account"],"responses":{"200":{"description":"Verified plan ceilings and independently complete or unknown quota totals.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsAccount","tags":["Account"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/templates":{"get":{"summary":"List templates","description":"The images a computer can be built from, with the CPU and memory each one defaults to and the disk it cannot be built below. For the templates we publish, `name` is what you pass as `template` to create; for one you published, pass its `ref` — a short name is not resolved to your templates and falls back to the default template, or, while this list is short and carries `X-GC-Incomplete`, is refused with a `503` (see `template` on `POST /computers`).\n\nIf a host cannot be reached the list may be short, and then carries `X-GC-Incomplete`; this listing answers with what it has rather than a `503`.\n\nA template is an image rather than an operating system — `hermes`, `claude` and `openclaw` are the base Linux desktop with an agent already installed, `coding` is the same desktop with ten coding-agent CLIs on it (Claude Code, Codex, Gemini CLI, Qwen Code, opencode, goose, Cline, Aider, Open Interpreter and a Cursor CLI installer), `managed-agents` runs Anthropic’s Claude Managed Agents self-hosted worker as a service once its environment key is bound as a secret, and `omarchy` is a different desktop entirely (Arch + Hyprland) — so read `label` rather than assuming this list is a choice of OS. Only the templates your plan may launch are returned.\n\n`omarchy` is a Wayland desktop, and that is worth knowing before you drive one. Screenshots, input and `exec` behave exactly as they do everywhere else: they are taken at the hypervisor, below the guest, so nothing about them changes. The window, window-action and clipboard routes are answered through the compositor instead of through X, which shows up in two places — a window id is the compositor address rather than an X window id (still opaque, still handed back the same way), and moving or resizing a window the compositor is TILING is refused rather than silently ignored, because a tiled window’s geometry belongs to the layout. Float it first.\n\nRequires the `viewer` role or higher.","operationId":"listTemplates","tags":["Templates"],"x-rate-cost":5,"responses":{"200":{"description":"Every template available to your account.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Template"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"name":"base","ref":"system/base@1.2.0","label":"Base Desktop (Linux)","os":"linux","icon":"debian","cpu":2,"ram_mb":2048,"disk_gb":20},{"name":"research-desktop","ref":"acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0","label":"Research desktop","os":"linux","cpu":2,"ram_mb":2048,"disk_gb":20}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/templates' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.templates.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.templates.listWithStatus();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List templates (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListTemplates","tags":["Templates"],"responses":{"200":{"description":"Every template available to your account.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"post":{"summary":"Publish a template","description":"Store a template document under a ref of your own, so `POST /computers` can launch it by name.\n\nACCOUNT-WIDE KEYS ONLY. Templates are the account’s catalogue, not a workspace’s: an API key confined to a workspace can launch a computer from a template by its ref and list them with `GET /templates`, but publishing, reading one of the account’s documents back and retiring are a `403` for it.\n\nTHE NAMESPACE IS YOUR ACCOUNT. `metadata.namespace` has to be your account id, which is the namespace part of the `ref` (`<account id>/name@version`) on every template `GET /templates` shows you as yours; anything else is a `403`, including `system`, which is reserved for the templates we publish. Your templates are private: no other account can see or launch them, and there is no registry.\n\nA REF IS IMMUTABLE. `namespace/name@version` names one document for ever. Publishing the identical document again succeeds and changes nothing, so a pipeline that republishes on every commit is safe; publishing a DIFFERENT document under the same ref is a `409`, and the fix is to bump `metadata.version`. What counts as different is the digest, so a changed label is a change.\n\nFOR EVER SURVIVES RETIRING. A ref you have retired with `DELETE /templates/{namespace}/{name}` is still spoken for: publishing it again is a `409` that names the date it went, and that is true even of the identical bytes. Retiring frees the ROW, which is what the ceiling counts — it does not free the name.\n\nThere is a ceiling on how many templates one account holds, and a publish past it is a `409` that says where you stand. Retire a version you no longer launch and publish again.\n\nThere is a SECOND, much larger ceiling on how many refs one account may ever claim — live and retired together. Retiring does not clear that one, because a retired ref can never be published again and so still counts. A `409` naming it is the one refusal here you cannot fix from your side; get in touch and we will raise it. `refs_claimed` on a retire response is where you stand.\n\nNONE OF THESE FOUR `409`s IS WORTH SENDING AGAIN AS IT IS, and each says so in `reason`: a different document under a taken ref and a retired ref carry `reason: \"exists\"` (bump `metadata.version`), and both ceilings carry `reason: \"unsupported\"` (retire a template, or get in touch).\n\nThe body is the document itself, as JSON or YAML, exactly as `POST /templates/validate` takes it — and validating first is worth it while you are iterating, since that route reports every problem at once and claims no ref. A document that is not valid is a `400` here, carrying the same list: `{error: \"That document is not valid.\", problems: [...]}`, every problem the validator found.\n\nA `spec.hardware.disk_gb` below the disk floor of the family, or of the `spec.from` parent, is a `400` naming the floor. The validator does not check this; publish a new version with at least that much.\n\nPUBLISHING IS NOT BUILDING, and what you can launch today follows from that. A document naming a `spec.family` the fleet already has an image for is launchable the moment it is published — which is how you give yourself one of our templates with your own label, sizing and defaults on it, and it needs no build at all.\n\nA DOCUMENT THAT DECLARES `spec.build` STEPS OR `spec.env` HAS TO NAME A FAMILY OF YOUR OWN, because a build WRITES into the family it names, and naming one that is not yours is a `403` — the same answer, and the same rule, as `POST /builds`. Yours are the ones named after your account: `golden-<your account id>`, or that and a `-` and a name of your choosing. A document with neither is under no such rule — it names a family to launch FROM, and naming one of ours is the point of it.\n\nA DOCUMENT THAT DECLARES `spec.build` STEPS OR `spec.env` MAY ONLY LAYER ONTO ONE OF OURS. `spec.from` has to name a template in the `system` namespace, and anything else is a `400` — here and at `POST /templates/validate`, which reports it first. A build layers onto the parent’s IMAGE, and a hypervisor resolves a parent only in the catalogue compiled into it: a build request carries the child document alone, so nothing there could work out what `your-account/base` is. Layering onto a template of your own is not supported yet. Note the asymmetry with the family rule above, because the two read alike and point opposite ways: the family you WRITE into must be yours, and the template you BUILD ONTO must be ours. A document with neither `spec.build` steps nor `spec.env` is under neither rule, and may record any `spec.from` it likes.\n\nAfter a successful `POST /builds`, create a computer using the published template ref. Each pinned template version selects the highest image version successfully built from that exact document; an unpinned ref first selects the newest published template version. Image versions are allocated fleet-wide and never reused after cleanup. A forced rebuild can advance the image selected for future creates; existing computers keep their original image. If no verified holder has capacity, a create prepares an on-demand replica and returns `409` with `code: template_image_preparing`, progress in `preparation`, and a `template_transfer` retry token. Repeat the create after `Retry-After` seconds with that token to keep the exact selected build. No computer is created while preparing. Placement uses only hosts with verified matching bytes and does not fall back to an older image if the selected build is unavailable. A missing, withdrawn or not-yet-verified image returns `409`; an incomplete fleet build inventory returns `503`. Builds made before custom-image launch support need the same document submitted once more to record its launch provenance; the normal reuse path can reuse matching image bytes.\n\nRequires the `member` role or higher.","operationId":"publishTemplate","tags":["Templates"],"x-rate-cost":5,"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary","description":"The template document itself, as JSON or YAML."}}}},"responses":{"201":{"description":"The template as stored. `document` is the canonical form — the bytes `doc_digest` is over — so it may differ from what you sent in key order and whitespace, and not in meaning.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishedTemplate"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ref":"acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0","doc_digest":"sha256:aa74e60a334f7128ec1fa919958c6a9d62f15b35bf46c0bb47b60e90a320413c","document":{"apiVersion":"mandala/v1","kind":"Template","metadata":{"namespace":"acc-d5e6f7a8b9c0d1e2","name":"research-desktop","version":"1.0.0","label":"Research desktop"},"spec":{"os":"linux","family":"golden-acc-d5e6f7a8b9c0d1e2-research-desktop","from":"system/base","build":[{"apt":{"packages":["jq"]}}],"hardware":{"cpu":2,"ram_mb":2048,"disk_gb":20}}},"template":{"name":"research-desktop","ref":"acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0","label":"Research desktop","os":"linux","cpu":2,"ram_mb":2048,"disk_gb":20},"versions":["1.0.0"],"published_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: templates are the account’s.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/templates' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/yaml\" \\\n  --data-binary @template.yaml"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    document = Path(\"template.yaml\").read_text(encoding=\"utf-8\")\n    result = client.templates.publish(document)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { readFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst document = await readFile(\"template.yaml\", \"utf8\");\nconst result = await client.templates.publish(document);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsTemplates","tags":["Templates"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/templates/schema":{"get":{"summary":"Get the template schema","description":"The JSON Schema for a `mandala/v1` template document — the declarative form a template is written in. Like every route here it needs an API key, so an editor or a `$ref` resolver pointed at this URL with no `Authorization` header gets a `401`. Fetch it with a key — `client.templates.schema()`, or `curl -H \"Authorization: Bearer $MANDALA_API_KEY\"` — save it to a file, and point your editor at that local copy for completion and validation while you write one.\n\nThe document describes what a template IS: its ref (`namespace/name@version`), the image family it resolves to, what it is layered onto, and the shape a computer gets when the create names no numbers. It describes how one is BUILT as well: `spec.build` is applied to the parent named by `spec.from` — apt installs, shell steps, files and directories, in the order written — and `spec.env` is baked into the image as a profile script, and `spec.secrets` names secrets a build step may read — by id, never by value. And it can give the computers created from it a default browser proxy, `spec.browser_proxy` — `{server, bypass}` under the rules of the create field of that name, which a create may override or clear with `null`. What it still does NOT describe is a build from nothing (every document layers over a parent; a base is built by a script outside this format) or lifecycle hooks, because nothing runs them: a field arrives here only once something executes it, which is why adding one is not a new `apiVersion` and a document written today keeps its meaning.\n\nRequires the `viewer` role or higher.","operationId":"getTemplateSchema","tags":["Templates"],"x-rate-cost":1,"responses":{"200":{"description":"The schema, as JSON Schema 2020-12.","content":{"application/json":{"schema":{"type":"object"},"examples":{"schema":{"summary":"Complete template JSON Schema response.","value":{"$id":"https://app.mandala.computer/api/v1/templates/schema","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"description":"A launch template: what a computer is built from and the shape it gets when the create names no numbers. mandala/v1 describes how one is BUILT as well: `spec.build` is applied to the parent named by `spec.from`, step by step in the order written, and `spec.env` is written into the image's profile. What is still absent is a build from nothing — every document layers over a parent — and lifecycle hooks, because nothing runs them; a field arrives under this apiVersion only once something executes it, and adding one does not make a v2.","properties":{"apiVersion":{"const":"mandala/v1","description":"The document format. A new value here means a field that already existed has changed meaning; fields being added does not."},"kind":{"const":"Template","description":"Always Template."},"metadata":{"additionalProperties":false,"description":"What the template is called. Nothing here reaches the image.","properties":{"label":{"description":"What a person is shown in the catalogue. Optional; a template with no label is shown by its name.","maxLength":64,"type":"string"},"name":{"description":"The template's name within its namespace. This is what a create's `template` field carries today.","maxLength":63,"not":{"pattern":"\\s"},"pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$","type":"string"},"namespace":{"description":"The publisher this template belongs to. `system` is the catalogue that ships with the product.","maxLength":63,"not":{"pattern":"\\s"},"pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$","type":"string"},"version":{"description":"MAJOR.MINOR.PATCH. A published ref is immutable: republishing namespace/name@version with any change at all is refused, so a change means a new version. Prerelease and build metadata are not accepted — they would be two names for one document.","not":{"pattern":"\\s"},"pattern":"^(0|[1-9][0-9]{0,8})\\.(0|[1-9][0-9]{0,8})\\.(0|[1-9][0-9]{0,8})$","type":"string"}},"required":["namespace","name","version"],"type":"object"},"spec":{"additionalProperties":false,"description":"What the template is.","properties":{"build":{"description":"What to do to the parent image, in order. Order is the semantics: the steps are applied in the sequence written. A template with no build steps is one whose image a script produced — see `family`.","items":{"oneOf":[{"additionalProperties":false,"properties":{"apt":{"additionalProperties":false,"description":"Install Debian packages.","properties":{"packages":{"items":{"not":{"pattern":"\\s"},"pattern":"^[a-z0-9][a-z0-9+.-]*[a-z0-9+](:[a-z0-9][a-z0-9-]*)?(=[A-Za-z0-9.+:~-]+|/[a-z0-9.-]+)?$","type":"string"},"minItems":1,"type":"array"},"recommends":{"description":"Install recommended packages too. Defaults to false, which is what you almost always want — a toolchain that drags in a documentation browser is a bigger image and a bigger attack surface. A desktop is the case that wants true.","type":"boolean"}},"required":["packages"],"type":"object"}},"required":["apt"],"type":"object"},{"additionalProperties":false,"properties":{"run":{"additionalProperties":false,"description":"Run a shell script inside the image.","properties":{"as":{"description":"Run as this user, through a login shell. Omit for root. Getting this wrong is the most consequential mistake in a build of this kind: an installer run as root puts the user's data in /root, where the desktop session — which logs in as someone else — will never see it, and the image looks installed and behaves uninstalled.","not":{"pattern":"\\s"},"pattern":"^$|^[a-z_][a-z0-9_-]*$","type":"string"},"script":{"not":{"pattern":"\u0000"},"pattern":"[^\\s]","type":"string"}},"required":["script"],"type":"object"}},"required":["run"],"type":"object"},{"additionalProperties":false,"properties":{"file":{"additionalProperties":false,"description":"Write a file into the image.","properties":{"content":{"type":"string"},"mode":{"description":"Octal, as a STRING — YAML reads a bare 0644 as the decimal 644, which is a mode nobody meant and chmod would accept.","not":{"pattern":"\\s"},"pattern":"^$|^0[0-7]{3}$","type":"string"},"owner":{"description":"`user:group`. Omit for root:root.","not":{"pattern":"\\s"},"pattern":"^$|^[a-z_][a-z0-9_-]*:[a-z_][a-z0-9_-]*$","type":"string"},"path":{"not":{"pattern":"(^|/)\\.\\.?(/|$)"},"pattern":"^/([^/\\x00:]+/)*[^/\\x00:]+$","type":"string"}},"required":["path"],"type":"object"}},"required":["file"],"type":"object"},{"additionalProperties":false,"properties":{"mkdir":{"additionalProperties":false,"description":"Create a directory, with parents.","properties":{"path":{"not":{"pattern":"(^|/)\\.\\.?(/|$)"},"pattern":"^/([^/\\x00:]+/)*[^/\\x00:]+$","type":"string"}},"required":["path"],"type":"object"}},"required":["mkdir"],"type":"object"}]},"type":["array","null"]},"desktop":{"description":"The display protocol this image's desktop speaks. Omit it for X11, which is what every Linux image built before this field runs. It decides which guest scripts the window, window-action and clipboard routes use: an X11 image is read with xprop and xdotool, a Wayland one through its compositor. Setting it wrongly does not break the desktop — it makes those three routes refuse, because the tools they reach for are not the ones the guest has.","enum":["x11","wayland"]},"env":{"additionalProperties":{"not":{"pattern":"[\n\u0000]"},"pattern":"^[^\n\u0000]*$","type":"string"},"description":"Environment variables baked into the image, written as a script in `/etc/profile.d` so every login shell in every session sees them. Rendered in key order, so the same document always produces the same bytes.","propertyNames":{"not":{"pattern":"\\s"},"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"type":["object","null"]},"family":{"description":"The golden image family this template resolves to, without a version suffix — the version a computer is pinned to is chosen at launch from the host's `<family>.current` pointer, so `golden-xfce` and never `golden-xfce-v6`. This field is the seam between a document and an image a build script produced, and the compiler removes it: a content-addressed template is named by its build digest. A document that declares build steps has to name a family of your OWN — `golden-<your account id>`, or that and a `-` and a name of your choosing — because a build WRITES into the family it names; a document with no steps names a family to launch FROM and is under no such rule. That is not checked here: this daemon has no accounts, so the answer comes from the control plane on publish and on build.","not":{"pattern":"-v[0-9]+$|\\s"},"pattern":"^golden-[a-z0-9]([a-z0-9-]*[a-z0-9])?$","type":"string"},"from":{"description":"The template this one is layered onto, as `namespace/name`. Records lineage, and is deliberately unversioned: the build follows the base family's `.current` pointer rather than a pin, so a version here would assert a guarantee the build does not make.\n\nA document that also declares `spec.build` or `spec.env` may only name a parent in the `system` namespace, and is refused otherwise — at validate and at publish. A build layers onto the parent's IMAGE, and a hypervisor resolves a parent only in the catalogue compiled into it: a build request carries the child document alone, so there is nothing that could tell it what `acme/base` is. Layering onto a template of your own is not supported yet. A document with no build steps is under no such rule — it records lineage for an image something else produced, and may name any parent.","not":{"pattern":"\\s"},"pattern":"^(?:$|[a-z0-9]([a-z0-9-]*[a-z0-9])?/[a-z0-9]([a-z0-9-]*[a-z0-9])?)$","type":"string"},"hardware":{"additionalProperties":false,"description":"The shape a computer gets when the create names no numbers. Not a constraint — what a computer may actually be given is bounded by the plan, and any size the plan allows is reachable at create or by a later resize.","properties":{"cpu":{"description":"Virtual CPUs.","maximum":2147483647,"minimum":1,"type":"integer"},"disk_gb":{"description":"Disk, in GiB. Unlike cpu and ram_mb this is a FLOOR rather than a default: a computer's disk is an overlay over the family's image and cannot be smaller than it, so a create asking for less is raised to this and charged for it.","maximum":2147483647,"minimum":1,"type":"integer"},"ram_mb":{"description":"Memory, in MiB.","maximum":2147483647,"minimum":512,"type":"integer"}},"required":["cpu","ram_mb","disk_gb"],"type":"object"},"icon":{"description":"A slug naming a mark the console has an asset for (\"claude\", \"debian\"), shown on the templates grid in place of a generic desktop icon. Purely cosmetic and never validated against a list published here: an unrecognised value, or none, just falls back to the plain icon. Not a URL — a document points at one of a small set the console ships assets for, rather than handing it an image to fetch and render.","maxLength":32,"not":{"pattern":"\\s"},"pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$","type":"string"},"os":{"description":"The guest's operating system. This is the field a plan is checked against: an account whose plan does not admit the OS cannot launch the template and is not shown it.","enum":["linux","windows"]},"readiness":{"additionalProperties":false,"description":"How this template says it has finished coming up: a command run in the guest, polled until it exits zero. Readiness is a property of what YOU installed — a desktop that has drawn, a server that is listening — and only you can state it, which is why there is a field for it rather than a heuristic. Omit it and the host will not capture a booted machine of this template, because nothing can tell it when to look; declare one and a launch resumes a running machine instead of cold-booting. What is checked HERE is the SHAPE — that there is a command, that `as` is a user name, that the timeout is in range. Whether the command can ever report your template ready is a question about your image, and nothing here runs it: `exec: false` is a valid document and a check that never passes.","properties":{"as":{"description":"Run the check as this user, through a login shell. Omit for root. A desktop's readiness is almost never visible to root — `systemctl --user`, the session bus and the process list of a graphical session all belong to the account that logged in.","not":{"pattern":"\\s"},"pattern":"^$|^[a-z_][a-z0-9_-]*$","type":"string"},"exec":{"description":"The command, run in the guest with `bash`. Exit zero means ready. A port check and an HTTP probe are both this — `ss -ltn | grep -q :8080`, `curl -fsS localhost:8080/health` — which is why there is one field here and not three. Without `as` it runs as root and does NOT get a login shell, so it inherits the guest agent's environment rather than a profile's — name binaries by path, or set `as` and get a login shell for that user.","not":{"pattern":"\u0000"},"pattern":"[^\\s]","type":"string"},"timeout_sec":{"description":"How long to keep asking before giving up, in seconds. Omit it, or give 0, for the default of 300 seconds; the most that can be asked for is 1800. It is a ceiling and not an expectation: the poll stops at the first exit zero, so a template ready in ten seconds waits ten seconds whatever this says.","maximum":1800,"minimum":0,"type":"integer"}},"required":["exec"],"type":["object","null"]}},"required":["os","family","hardware"],"type":"object"}},"required":["apiVersion","kind","metadata","spec"],"title":"Mandala Computer template","type":"object"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/templates/schema' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.templates.schema()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.templates.schema();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get the template schema (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetTemplateSchema","tags":["Templates"],"responses":{"200":{"description":"The schema, as JSON Schema 2020-12.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsTemplatesSchema","tags":["Templates"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/templates/validate":{"post":{"summary":"Check a template document","description":"Check a template document against the schema and the rules the publish path applies, without publishing anything. Nothing is stored and no ref is claimed, so this is safe to call on a draft and safe to call repeatedly.\n\nThe body is the document itself, as JSON or YAML — not a JSON wrapper around it. Send `Content-Type: application/yaml` or `application/json`; either is read.\n\nA document that is wrong is reported with **every** problem one pass can reach rather than the first, so a file with four mistakes takes one call — see `problems` in the response for the one case where fixing a problem reveals another. On success the two digests come back: `doc_digest` identifies the document and changes with anything that changes what it MEANS, and `build_digest` covers only what decides the image — so a new label or a version bump leaves it alone. `build_digest` is present only for a document with no `spec.from`, and such a document builds nothing: `spec.build` and `spec.env` need a parent. A document that names a parent in `spec.from` — which is every document that builds — has no `build_digest`, because that cannot be computed without the parent image’s, so comparing it cannot tell you whether an edit to such a document means a rebuild.\n\nRequires the `member` role or higher.","operationId":"validateTemplate","tags":["Templates"],"x-rate-cost":1,"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary","description":"The template document itself, as JSON or YAML."}}}},"responses":{"200":{"description":"`valid` is what you branch on, and both branches are 200: an invalid document is an answer to the question, not a failed request. `false` brings `problems` and nothing else; `true` brings the ref, the digests, the catalogue row and the canonical bytes.\n\nThree of the fields below — `template`, `canonical` and `build_digest_needs` — were on this wire and in no reader’s copy of it until OPL-4179, which wrote them out here in prose. OPL-4190 gave the response a schema, so the table is now held against the code that produces it rather than kept in step by hand. In the same change `template` stopped carrying the on-host image `family`, which is ours rather than yours and was already projected away on every other route that answers a catalogue row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateValidation"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"valid":true,"ref":"acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0","doc_digest":"sha256:aa74e60a334f7128ec1fa919958c6a9d62f15b35bf46c0bb47b60e90a320413c","template":{"name":"research-desktop","ref":"acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0","label":"Research desktop","os":"linux","cpu":2,"ram_mb":2048,"disk_gb":20},"canonical":"{\"apiVersion\":\"mandala/v1\",\"kind\":\"Template\",\"metadata\":{\"namespace\":\"acc-d5e6f7a8b9c0d1e2\",\"name\":\"research-desktop\",\"version\":\"1.0.0\",\"label\":\"Research desktop\"},\"spec\":{\"os\":\"linux\",\"family\":\"golden-acc-d5e6f7a8b9c0d1e2-research-desktop\",\"from\":\"system/base\",\"build\":[{\"apt\":{\"packages\":[\"jq\"]}}],\"hardware\":{\"cpu\":2,\"ram_mb\":2048,\"disk_gb\":20}}}","build_digest_needs":"the contents of system/base's image, which only a host holding that image can compute; the digest is computed when the build runs"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/templates/validate' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/yaml\" \\\n  --data-binary @template.yaml"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    document = Path(\"template.yaml\").read_text(encoding=\"utf-8\")\n    result = client.templates.validate(document)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { readFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst document = await readFile(\"template.yaml\", \"utf8\");\nconst result = await client.templates.validate(document);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsTemplatesValidate","tags":["Templates"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/templates/{namespace}/{name}":{"get":{"summary":"Get a template","description":"One template, as the document it was written as — which is the half `GET /templates` drops: the lineage in `spec.from`, the build steps, and the digest.\n\nWorks for your own namespace and for `system`, so you can read what you are layering onto — though note that a document with `spec.build` steps or `spec.env` may only layer onto a `system` template, so reading your own is for inspecting it rather than for building on it. See `POST /templates`. Another account’s namespace is a `404`, the same answer a name that does not exist gets.\n\nYour account’s own namespace needs an account-wide key: an API key confined to a workspace is a `403` there, because the document is the account’s and may carry its build steps and scripts. It can still read `system` templates.\n\nWithout `version` this is the newest published version of that name — which is also what a create naming the unpinned `namespace/name` resolves to. `versions` lists the rest, newest first.\n\nA ref you have RETIRED is still a `404` — there is no document to return — but the message names the date it went rather than telling you the template never existed. Same for a name whose every version has been retired.\n\nRequires the `member` role or higher.","operationId":"getTemplate","tags":["Templates"],"x-rate-cost":1,"parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"},"description":"Your account id, or `system` for the templates we publish."},{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"The template’s `metadata.name` — the part of the ref between the slash and the `@`."},{"name":"version","in":"query","required":false,"schema":{"type":"string"},"description":"A specific `MAJOR.MINOR.PATCH`. Omit it for the newest. Sending it EMPTY or malformed is a `400` rather than a silent default — the same rule `DELETE` applies, where it matters much more."}],"responses":{"200":{"description":"The template.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishedTemplate"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ref":"system/base@1.2.0","doc_digest":"sha256:e751ce0d9fd496887370989733757bed5645560a0cbdb5c99aeac6d22b9d75a8","document":{"apiVersion":"mandala/v1","kind":"Template","metadata":{"namespace":"system","name":"base","version":"1.2.0","label":"Base Desktop (Linux)"},"spec":{"os":"linux","family":"golden-xfce","icon":"debian","readiness":{"exec":"/usr/bin/pgrep -x xfdesktop >/dev/null && sleep 2 && /usr/bin/pgrep -x xfdesktop >/dev/null","timeout_sec":240},"hardware":{"cpu":2,"ram_mb":2048,"disk_gb":20}}},"template":{"name":"base","ref":"system/base@1.2.0","label":"Base Desktop (Linux)","os":"linux","icon":"debian","cpu":2,"ram_mb":2048,"disk_gb":20},"versions":["1.2.0"]}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to an API key confined to a workspace asking for a template in your account’s own namespace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/templates/system/base' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    namespace = \"system\"\n    name = \"base\"\n    result = client.templates.get(namespace, name)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst namespace = \"system\";\nconst name = \"base\";\nconst result = await client.templates.get(namespace, name);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get a template (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetTemplate","tags":["Templates"],"parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"},"description":"Your account id, or `system` for the templates we publish."},{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"The template’s `metadata.name` — the part of the ref between the slash and the `@`."},{"name":"version","in":"query","required":false,"schema":{"type":"string"},"description":"A specific `MAJOR.MINOR.PATCH`. Omit it for the newest. Sending it EMPTY or malformed is a `400` rather than a silent default — the same rule `DELETE` applies, where it matters much more."}],"responses":{"200":{"description":"The template.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to an API key confined to a workspace asking for a template in your account’s own namespace.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"delete":{"summary":"Retire a template","description":"Retire a template you published, so it stops resolving and stops counting against your ceiling.\n\nWITH `version` this retires that one version. WITHOUT it, this retires EVERY version of the name — which is what “retire this template” means, and is deliberately not the read route’s “the newest”: a delete that quietly took the latest one would let a script walk backwards through a history it never asked about.\n\nCOMPUTERS ARE NOT AFFECTED. A computer is built from the IMAGE the ref resolved to and holds no reference to the document, so anything already running, stopped or suspended is untouched, before and after. What a retire breaks is resolution: a NEW create naming the ref is refused.\n\nTHE REF IS STILL SPOKEN FOR, AND STILL COUNTS ONCE. Retiring frees the row the `templates` ceiling counts; it does not free the name, and it does not reduce `refs_claimed` — the much larger ceiling on refs an account may ever claim. Publishing `namespace/name@version` again after retiring it is a `409`, identical bytes included — a ref that resolves, then does not, then does again is worse than either. Publish the next version instead.\n\nA ref that was never yours is a `403`, including `system`. One you already retired is a `404` that says when it went. An API key confined to a workspace is a `403` whatever it names: retiring is for an account-wide key.\n\nOMITTING `version` AND SENDING IT EMPTY ARE DIFFERENT. `?version=` — which is what most clients send for an unset optional string — is a `400`, not a request to retire the whole name. So is any value that is not a `MAJOR.MINOR.PATCH`. Leaving the parameter off the URL entirely is the only way to ask for every version.\n\nRequires the `member` role or higher.","operationId":"retireTemplate","tags":["Templates"],"x-rate-cost":1,"parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"},"description":"Your account id, or `system` for the templates we publish."},{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"The template’s `metadata.name` — the part of the ref between the slash and the `@`."},{"name":"version","in":"query","required":false,"schema":{"type":"string"},"description":"A specific `MAJOR.MINOR.PATCH`. Omit the parameter entirely to retire every version of this name; sending it empty or malformed is a `400`."}],"responses":{"200":{"description":"What went, what is left of the name, and how many templates the account now holds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetiredTemplates"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"retired":["acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0"],"retired_at":"2026-09-16T12:00:01.000Z","versions":[],"templates":0,"refs_claimed":1}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: templates are the account’s.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/templates/acc-d5e6f7a8b9c0d1e2/research-desktop?version=1.0.0' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    namespace = \"acc-d5e6f7a8b9c0d1e2\"\n    name = \"research-desktop\"\n    query = {\"version\":\"1.0.0\"}\n    result = client.templates.retire(namespace, name, version=query[\"version\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst namespace = \"acc-d5e6f7a8b9c0d1e2\";\nconst name = \"research-desktop\";\nconst query = {\"version\":\"1.0.0\"};\nconst result = await client.templates.retire(namespace, name, {version: query.version});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsTemplatesByNamespaceByName","tags":["Templates"],"security":[],"parameters":[{"name":"namespace","in":"path","required":true,"schema":{"type":"string"},"description":"Your account id, or `system` for the templates we publish."},{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"The template’s `metadata.name` — the part of the ref between the slash and the `@`."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/builds":{"get":{"summary":"List builds","description":"Every build this account has started that the fleet still holds a record of, newest first. Needs an account-wide key: one confined to a workspace is a `403`.\n\nThis is a fan-out across the fleet, so it fails closed the way `GET /computers` and `GET /snapshots` do: a hypervisor that cannot be reached makes the answer 503 rather than short. `allow_partial` is the way through.\n\nRequires the `viewer` role or higher.","operationId":"listBuilds","tags":["Builds"],"x-rate-cost":5,"parameters":[{"name":"allow_partial","in":"query","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Accept a listing known to be short. Without it this endpoint answers 503 when a hypervisor holding some of your things cannot be reached, because a short list is not a smaller truth — it reads exactly like the missing ones were deleted, and the obvious next thing a script does with something that has disappeared is tidy it up. A short build listing has no rows marking what is gone: the builds a silent host was holding are simply not there. Nothing on this tier records which hypervisor ran which build — a build is not a thing you act on afterwards, so there was never anything to route to it — so there is no cache to name what is missing here, and no count to give you either. Read the `X-GC-Incomplete` response header rather than the rows: it is present only when the listing is short, and its value here is always `0`, because nothing can count what is missing. Any other value of this parameter is read as absent rather than refused."}],"responses":{"200":{"description":"Your builds.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TemplateBuild"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"bld-b1c2d3e4f5a6","ref":"acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0","status":"running","started_at":"2026-09-16T12:00:00.000Z"}]}}}},"headers":{"X-GC-Incomplete":{"description":"Present only on a listing known to be short, which is answered only when `allow_partial` was passed. Always `0` here: nothing records which hypervisor holds which build, so nothing can count what is missing.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Incomplete":{"description":"Present when this `503` is the refusal of a short listing; pass `allow_partial=1` to be answered with the short listing instead. Always `0` here.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/builds' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.builds.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.builds.listWithStatus();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List builds (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListBuilds","tags":["Builds"],"parameters":[{"name":"allow_partial","in":"query","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Accept a listing known to be short. Without it this endpoint answers 503 when a hypervisor holding some of your things cannot be reached, because a short list is not a smaller truth — it reads exactly like the missing ones were deleted, and the obvious next thing a script does with something that has disappeared is tidy it up. A short build listing has no rows marking what is gone: the builds a silent host was holding are simply not there. Nothing on this tier records which hypervisor ran which build — a build is not a thing you act on afterwards, so there was never anything to route to it — so there is no cache to name what is missing here, and no count to give you either. Read the `X-GC-Incomplete` response header rather than the rows: it is present only when the listing is short, and its value here is always `0`, because nothing can count what is missing. Any other value of this parameter is read as absent rather than refused."}],"responses":{"200":{"description":"Your builds.","headers":{"X-GC-Incomplete":{"description":"Present only on a listing known to be short, which is answered only when `allow_partial` was passed. Always `0` here: nothing records which hypervisor holds which build, so nothing can count what is missing.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-GC-Incomplete":{"description":"Present when this `503` is the refusal of a short listing; pass `allow_partial=1` to be answered with the short listing instead. Always `0` here.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"post":{"summary":"Build a template","description":"Compile a template document into a golden image, and return immediately with a job to watch. A build takes minutes — an agent image is roughly fifteen — so this never blocks: the answer is `202` with an `id`, and `GET /builds/{id}` says what became of it.\n\nACCOUNT-WIDE KEYS ONLY. Builds are the account’s, like the templates they build: an API key confined to a workspace is a `403` on this route and on every other `/builds` route, and can launch a computer from a published template by its ref instead.\n\nThe body is the document itself, as JSON or YAML, exactly as `POST /templates/validate` takes it. Validate first if you are iterating; a document that is wrong is refused here too, but the validator tells you every problem at once.\n\nTHE NAMESPACE AND THE FAMILY BOTH HAVE TO BE YOURS, and either one is a `403`. `metadata.namespace` has to be your account id, the same rule `POST /templates` states. `spec.family` is what the built image is CALLED on a hypervisor, in a directory shared with every computer on that machine — so a build may only write into a family named after your account: `golden-<your account id>`, or that and a `-` and a name of your choosing. The message names the family it refused.\n\nTHE PARENT HAS TO BE ONE OF OURS. `spec.from` on a document with `spec.build` steps or `spec.env` must name a `system` template, and anything else is a `400` rather than a `403`: it is not a permission, it is that no hypervisor can resolve it. A build layers onto the parent’s image and the only catalogue a hypervisor searches is the one compiled into it, while this request carries your document alone — so there is nothing that could tell it what `your-account/base` is. Layering onto a template of your own is not supported yet.\n\nSECRETS ARE NAMED, NEVER WRITTEN. `spec.secrets` lists the secrets a build step may read: each entry is the `id` of one of yours and the `as` it appears under as an environment variable. The value is not in the document and never travels in it — a template document is text you publish, copy and review, and a value written there is a value in a file. The values are delivered to the build over a channel of their own and a step sees them only as those variables: we put them in no argument, no staged file and no line of the build log. What a step does with them afterwards is yours — a step that prints its own environment prints the value, and the build log is readable through `GET /builds/{id}/progress`.\n\nA document may name at most 32 secrets; more is a `400` here. `POST /templates/validate` does not check that limit, so a document it passes can still be refused for it.\n\nA reference is resolved in YOUR scope at the moment you submit, and the revision current then is the one the build uses: your account-wide secrets. A reference that does not resolve in that scope — a workspace’s, another account’s, one that has been deleted — is a `400` carrying ONE sentence, the same sentence for all three, because which of them it was is not something an answer should tell you. That is the only refusal that is deliberately indistinct: a malformed reference gets a `400` saying what is wrong with it, a secret whose value cannot be delivered gets a `409`, and a deployment with no keyring configured gets a `503`. Retry logic wants those apart — the `503` is worth retrying, and the two `400`s and that `409` are not. The secret `409` carries `reason: \"unsupported\"` and is final until the secret is replaced: its value cannot be read under this control plane’s keyring, or is not text an environment variable can carry, or the secret was deleted or left the build’s scope while the request was being admitted. Replace the secret (or name another) and build again.\n\nFreezing the revision is what makes a build reproducible and what makes a rotation land. Two builds of the same document against the same revisions are the same build and the second may reuse the first’s image; rotate a secret and the next build is a different build, and runs. An image built from your secrets is yours alone — another account writing the same document never reaches it, and could not name your secret to begin with.\n\nOne build runs per hypervisor at a time. A `409` with `reason: \"contention\"` means a host is busy, or has no room on its disk for another build right now, rather than that anything is wrong with your document, and is worth retrying. Your plan sets how many builds you may start in a rolling day; a `429` names the number you have used.\n\nA BUILD YOU KEEP COUNTS AGAINST YOUR STORAGE POOL. A build produces a whole disk image that stays on a hypervisor, so each one your account keeps counts against your plan’s storage pool — the same pool your computers’ disks use — at its template’s `disk_gb`. A build that would take the pool past its size is a `402` that names the pool, your computers and your kept builds, and costs nothing; creating or growing a computer counts kept builds the same way. A build counts from the moment it is accepted, and stops counting if it fails or reuses an image already built. Each template family keeps its three newest builds on each hypervisor; an older one is removed automatically once no computer or snapshot uses it, and then stops counting — so launching a template whose build was removed is refused until you build it again. Builds made before this rule are kept, never removed to make room: an account over its pool because of them has new builds and computers that need room refused, and its owners are told once by email which families hold what.\n\nA BUILD THAT REUSED AN IMAGE DOES NOT SPEND THAT ALLOWANCE. An identical document, built before, is answered from the image already on disk in a fraction of a second rather than the minutes a real build takes, so resubmitting bytes you have already built does not spend a unit sized against the long case. It is not counted once the fleet reports what it was, which follows the `202` by a moment rather than arriving with it. Passing `no_reuse` opts out: it is a request for the work, and the work is charged.\n\nTHERE IS A SECOND, MUCH LARGER CEILING ON BUILD REQUESTS, ten times the first. Every request that reaches a hypervisor counts against it, whatever comes of it there — a reuse hit is cheap and it is not free: it takes that machine’s one build slot while it runs, and every other build on it is answered `409` for the length of that. Requests refused before any hypervisor is asked — a plan that does not include building, a namespace or a family that is not yours — count against neither ceiling, because nothing was asked of any machine. A document that will not PARSE is not one of those: it is sent on, because the hypervisor that would build it is the tier that reads it, so it costs a request and no build. Both ceilings answer `429`, and the message says which one you met. Iterating on a document will not come near the second; a loop resubmitting one will.\n\nA REQUEST A HYPERVISOR REFUSES COSTS NO BUILD — `409` busy or out of room, `400` for a document it will not build, `501` — and it does count as a request: the host started nothing, but it was asked. A build that was admitted (`202`) counts against the build allowance whatever becomes of it, a build that FAILED included — what that one bounds is a shared machine, and charging only for the builds that worked would make the cheapest way to exhaust a hypervisor a document that always fails. So does a build this API could not get an answer about, since a hypervisor may well be running it.\n\nRequires the `member` role or higher.","operationId":"startBuild","tags":["Builds"],"x-rate-cost":100,"parameters":[{"name":"no_reuse","in":"query","required":false,"schema":{"type":"string","enum":["true"]},"description":"Build even when an image already carries this document’s build digest. Identical documents normally share an image, which is what makes a repeated build cheap; pass this when you want the work done again anyway. This one does count against your daily allowance whatever it finds — the work is the thing being asked for."}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary","description":"The template document itself, as JSON or YAML, at most 64 KiB; a larger body is a `413` and nothing is built."}}}},"responses":{"202":{"description":"The job. `status` is `running` until it is `succeeded` or `failed`. A succeeded build has produced an image. Publish that same document, then create a computer using its `ref`. Launches select the newest successful image for that exact document and verify it on its host; see `POST /templates` for version selection and unavailable-image behavior.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateBuild"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"bld-b1c2d3e4f5a6","ref":"acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0","status":"running","started_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nTwo kinds here, and `reason` tells them apart. `reason: \"unsupported\"`: a secret the document names cannot be delivered (its value cannot be read under this control plane’s keyring, or is not text an environment variable can carry, or the secret was deleted or left the build’s scope); nothing was built, and sending it again never helps until the secret is replaced. `reason: \"contention\"`: every host that can build is busy with another build, or has no room in its memory or on its disk for one; nothing was built, and the same request can succeed shortly. `unsupported` too, when the only builders that answered have disk bookkeeping that needs operator recovery, which waiting does not clear.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"501":{"description":"This feature is not available: no hypervisor in the fleet can build a template, or the platform or the host holding this computer does not offer it. Unlike a `409`, this does not clear on its own — retrying will not help, so treat it as an outage of the feature rather than of the moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/builds' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/yaml\" \\\n  --data-binary @template.yaml"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    document = Path(\"template.yaml\").read_text(encoding=\"utf-8\")\n    result = client.builds.start(document)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { readFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst document = await readFile(\"template.yaml\", \"utf8\");\nconst result = await client.builds.start(document);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsBuilds","tags":["Builds"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/builds/{id}":{"get":{"summary":"Get a build","description":"What became of one build. The golden’s own filename is ours and is accepted nowhere here; what names the result is the `ref` you submitted: publish that same document with `POST /templates` and create a computer by its ref — see there for how the image is chosen and the `template_image_preparing` 409. `error` says why a failed one failed, which for a `run:` step is the end of that step’s own output.\n\nNeeds an account-wide key: one confined to a workspace is a `403`.\n\nRequires the `viewer` role or higher.","operationId":"getBuild","tags":["Builds"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"200":{"description":"The build.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateBuild"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"bld-b1c2d3e4f5a6","ref":"acc-d5e6f7a8b9c0d1e2/research-desktop@1.0.0","status":"running","started_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/builds/bld-b1c2d3e4f5a6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"bld-b1c2d3e4f5a6\"\n    result = client.builds.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"bld-b1c2d3e4f5a6\";\nconst result = await client.builds.get(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get a build (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetBuild","tags":["Builds"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"200":{"description":"The build.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsBuildsById","tags":["Builds"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/builds/{id}/progress":{"get":{"summary":"Get build progress","description":"What a build is DOING, as against what became of it. A build is minutes long — most of them spent copying a multi-gigabyte base image and then running your document’s steps — so this says which step of how many is running, and which one failed.\n\nThis is the polling half. `GET builds/{id}/events` is the same thing as an event stream; use that for a terminal and this for anything that reconnects, restarts, or cannot hold a socket open.\n\nIt stays readable after the build has finished, so a program that was not attached at the time can still see which step failed.\n\nNeeds an account-wide key: one confined to a workspace is a `403`.\n\nRequires the `viewer` role or higher.","operationId":"getBuildProgress","tags":["Builds"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"200":{"description":"Where the build has got to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildProgress"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"bld-b1c2d3e4f5a6","status":"running","done":false,"phase":"building","step":1,"of":2,"steps":[{"n":1,"kind":"apt","label":"jq","status":"running","started_at":"2026-09-16T12:00:01.000Z"},{"n":2,"kind":"finish","label":"finishing off","status":"pending"}],"note":"Installing packages","updated_at":"2026-09-16T12:00:01.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/builds/bld-b1c2d3e4f5a6/progress' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"bld-b1c2d3e4f5a6\"\n    result = client.builds.progress(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"bld-b1c2d3e4f5a6\";\nconst result = await client.builds.progress(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get build progress (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetBuildProgress","tags":["Builds"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"200":{"description":"Where the build has got to.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsBuildsByIdProgress","tags":["Builds"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/builds/{id}/events":{"get":{"summary":"Stream build progress","description":"The same record as `GET builds/{id}/progress`, as `text/event-stream`, for as long as the build runs, up to 50 minutes. A build cannot run that long, so a stream still open then ends with an `error` event rather than closing quietly.\n\nThree event types. `progress` carries a `BuildProgress` and is sent only when something actually moved, so every one of them is news. `done` carries the final `BuildProgress` and is the last event of a build that finished — including one that FAILED, which is a `done` whose `status` says `failed` rather than an `error`. `error` is the last event of a stream that could not go on, and says nothing about the build. Its data is `{request_id, error}` when the hypervisor running the build stopped answering or the stream reached its 50 minutes; poll for the outcome. When your authority went while the stream was open — the key deleted, the membership removed, the account suspended — it also carries `status` (`401` or `403`) and `reason: \"revoked\"`, the refusal the same request would get now; polling is refused the same way, so do not poll with that credential.\n\nAttaching to a build that has already finished is not an error: you get one `progress` and one `done` immediately. Lines beginning `:` are keepalives and can be ignored.\n\nAn account may hold eight of these open at once; the ninth is refused with a `429` naming the limit. It is a bound on concurrency rather than on price, because a stream is charged once and then polls for as long as the build runs. There is one build per hypervisor, so this is more streams than a fleet has builds to watch.\n\nNeeds an account-wide key: one confined to a workspace is a `403`.\n\nRequires the `viewer` role or higher.","operationId":"streamBuildProgress","tags":["Builds"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"200":{"description":"The payload of each `progress` and `done` event. The response body is the event stream.","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/BuildProgress"},"examples":{"success":{"summary":"Illustrative SSE progress and terminal done frames.","value":": keepalive\n\nevent: progress\ndata: {\"id\":\"bld-b1c2d3e4f5a6\",\"status\":\"running\",\"done\":false,\"phase\":\"building\",\"step\":1,\"of\":2,\"steps\":[{\"n\":1,\"kind\":\"apt\",\"label\":\"jq\",\"status\":\"running\",\"started_at\":\"2026-09-16T12:00:01.000Z\"},{\"n\":2,\"kind\":\"finish\",\"label\":\"finishing off\",\"status\":\"pending\"}],\"note\":\"Installing packages\",\"updated_at\":\"2026-09-16T12:00:01.000Z\"}\n\nevent: done\ndata: {\"id\":\"bld-b1c2d3e4f5a6\",\"status\":\"succeeded\",\"done\":true,\"phase\":\"published\",\"step\":2,\"of\":2,\"steps\":[{\"n\":1,\"kind\":\"apt\",\"label\":\"jq\",\"status\":\"done\",\"started_at\":\"2026-09-16T12:00:01.000Z\",\"finished_at\":\"2026-09-16T12:00:02.000Z\"},{\"n\":2,\"kind\":\"finish\",\"label\":\"finishing off\",\"status\":\"done\",\"started_at\":\"2026-09-16T12:00:02.000Z\",\"finished_at\":\"2026-09-16T12:00:02.000Z\"}],\"note\":\"Image published\",\"updated_at\":\"2026-09-16T12:00:02.000Z\"}\n\n"},"stream-error":{"summary":"Illustrative error frame after the HTTP stream has opened; work may be incomplete.","value":"event: error\ndata: {\"request_id\":\"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17\",\"error\":\"the hypervisor running this build stopped answering; poll the build for its outcome\"}\n\n"}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nON THIS ROUTE a 429 is usually the eight-streams-at-once cap rather than your rate budget, and the message names which limit you met. Both carry `Retry-After`, but it means something weaker here: the meter computes when your budget refills, while the stream cap can only offer a poll interval — a slot frees when somebody else’s build ends, and nothing can say when that is. So waiting out `Retry-After` on this one is not a promise of a slot. The durable answers are to close a stream you already hold, or to poll `GET /builds/{id}/progress`, which has no cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/builds/bld-b1c2d3e4f5a6/events' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  --no-buffer"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom contextlib import closing\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"bld-b1c2d3e4f5a6\"\n    with closing(client.builds.events(id)) as events:\n        for event in events:\n            print(event)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"bld-b1c2d3e4f5a6\";\nfor await (const event of client.builds.events(id)) console.log(event);"}]},"head":{"summary":"Stream build progress (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. This one checks metadata instead of reading the content: it never resumes a guest, consumes output or opens a stream, so a header or failure that only reading the content could produce may be missing.\n\nRequires the `viewer` role or higher.","operationId":"headStreamBuildProgress","tags":["Builds"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"200":{"description":"The payload of each `progress` and `done` event. The response body is the event stream.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace: builds are the account’s.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nON THIS ROUTE a 429 is usually the eight-streams-at-once cap rather than your rate budget, and the message names which limit you met. Both carry `Retry-After`, but it means something weaker here: the meter computes when your budget refills, while the stream cap can only offer a poll interval — a slot frees when somebody else’s build ends, and nothing can say when that is. So waiting out `Retry-After` on this one is not a promise of a slot. The durable answers are to close a stream you already hold, or to poll `GET /builds/{id}/progress`, which has no cap.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsBuildsByIdEvents","tags":["Builds"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The build id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/sizes":{"get":{"summary":"List sizes","description":"The named sizes a computer can be launched at — each is a template plus a CPU/RAM/disk shape. These are the shapes hosts keep pre-booted, so a create that names one is typically answered from the warm pool in about a second; a custom shape boots cold in ten or so. Pass one as `size` on `POST /computers`. The catalogue is not a constraint: sending explicit `cpu`/`ram_mb`/`disk_gb` instead remains allowed, and a resize can take a computer anywhere your plan permits afterwards.\n\nRequires the `viewer` role or higher.","operationId":"listSizes","tags":["Templates"],"x-rate-cost":1,"responses":{"200":{"description":"The catalogue, with what your plan admits.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Size"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"small","label":"Small","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"allowed":true,"cheapest_plan":"solo"},{"id":"standard","label":"Standard","template":"base","cpu":2,"ram_mb":4096,"disk_gb":20,"allowed":true,"cheapest_plan":"solo"},{"id":"large","label":"Large","template":"base","cpu":4,"ram_mb":8192,"disk_gb":40,"allowed":true,"cheapest_plan":"solo"},{"id":"hermes-standard","label":"Standard","template":"hermes","cpu":2,"ram_mb":4096,"disk_gb":20,"allowed":true,"cheapest_plan":"solo"},{"id":"claude-standard","label":"Standard","template":"claude","cpu":2,"ram_mb":4096,"disk_gb":20,"allowed":true,"cheapest_plan":"solo"},{"id":"claude-large","label":"Large","template":"claude","cpu":4,"ram_mb":8192,"disk_gb":40,"allowed":true,"cheapest_plan":"solo"},{"id":"omarchy-standard","label":"Standard","template":"omarchy","cpu":2,"ram_mb":4096,"disk_gb":20,"allowed":true,"cheapest_plan":"solo"},{"id":"openclaw-standard","label":"Standard","template":"openclaw","cpu":2,"ram_mb":4096,"disk_gb":20,"allowed":true,"cheapest_plan":"solo"}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/sizes' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.sizes.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.sizes.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List sizes (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListSizes","tags":["Templates"],"responses":{"200":{"description":"The catalogue, with what your plan admits.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSizes","tags":["Templates"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers":{"get":{"summary":"List computers","description":"Every computer on the account, or — when the key is scoped to one workspace — every computer in that workspace. `workspace_id` narrows it to one workspace, or to the computers in none. No `vnc` on these rows; fetch one computer to get its desktop credentials.\n\nRequires the `viewer` role or higher.","operationId":"listComputers","tags":["Computers"],"x-rate-cost":5,"parameters":[{"name":"allow_partial","in":"query","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Accept a listing known to be short. Without it this endpoint answers 503 when a hypervisor holding some of your things cannot be reached, because a short list is not a smaller truth — it reads exactly like the missing ones were deleted, and the obvious next thing a script does with something that has disappeared is tidy it up. Rows served this way carry `unreachable: true` and the identity the platform keeps for the computer, and nothing its host alone knows."},{"name":"state","in":"query","required":false,"schema":{"type":"string","enum":["live","unreachable","deleting","deleted","lost"]},"description":"Only computers in this lifecycle state — see `state` on Computer. Without it the listing is every computer that exists or may exist: `live`, `unreachable` and `deleting`. `deleted` and `lost` rows are answered from the platform’s record alone, since no host has them to list, and this parameter is the only way they are shown. An `unreachable` row is one no host answered for, which makes the listing short, so `state=unreachable` without `allow_partial=1` answers either `[]` or a `503`."},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"Only the computers in this workspace — an id from `GET /workspaces` — or, with `unassigned`, only the computers in no workspace. A workspace id answers exactly what a key scoped to that workspace would list, the platform’s own rows for unreachable and ended computers included. It combines with `state`. A workspace this key cannot reach — another account’s, one that does not exist, or any but its own for a key scoped to a workspace — is a `404`, and so is `unassigned` from a scoped key, which cannot see those computers at all."}],"responses":{"200":{"description":"Your computers.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Computer"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"vm-a1b2c3d4e5f6","name":"scratch","status":"running","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"running_ram_mb":2048,"resolution":"1280x800x24","created_at":"2026-09-16T12:00:00.000Z","state":"live"}]}}}},"headers":{"X-GC-Incomplete":{"description":"Present only on a listing known to be short, which is answered only when `allow_partial` was passed: a host did not answer, or some rows were served from the platform’s own record rather than by their host. Its value is how many of the computers in the scope asked for no host answered for — the rows served from the platform’s own record, each carrying `unreachable: true` — counted before `state` filters the rows, so with `state=live` none of them is in the answer. It can be `0`, which means the listing may be short by computers the platform cannot name.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Incomplete":{"description":"Present when this `503` is the refusal of a short listing; pass `allow_partial=1` to be answered with the short listing instead. Its value is how many of the computers in the scope asked for no host answered for — the rows served from the platform’s own record, each carrying `unreachable: true` — counted before `state` filters the rows, so with `state=live` none of them is in the answer. It can be `0`, which means the listing may be short by computers the platform cannot name.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.computers.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.computers.listWithStatus();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List computers (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListComputers","tags":["Computers"],"parameters":[{"name":"allow_partial","in":"query","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Accept a listing known to be short. Without it this endpoint answers 503 when a hypervisor holding some of your things cannot be reached, because a short list is not a smaller truth — it reads exactly like the missing ones were deleted, and the obvious next thing a script does with something that has disappeared is tidy it up. Rows served this way carry `unreachable: true` and the identity the platform keeps for the computer, and nothing its host alone knows."},{"name":"state","in":"query","required":false,"schema":{"type":"string","enum":["live","unreachable","deleting","deleted","lost"]},"description":"Only computers in this lifecycle state — see `state` on Computer. Without it the listing is every computer that exists or may exist: `live`, `unreachable` and `deleting`. `deleted` and `lost` rows are answered from the platform’s record alone, since no host has them to list, and this parameter is the only way they are shown. An `unreachable` row is one no host answered for, which makes the listing short, so `state=unreachable` without `allow_partial=1` answers either `[]` or a `503`."},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"Only the computers in this workspace — an id from `GET /workspaces` — or, with `unassigned`, only the computers in no workspace. A workspace id answers exactly what a key scoped to that workspace would list, the platform’s own rows for unreachable and ended computers included. It combines with `state`. A workspace this key cannot reach — another account’s, one that does not exist, or any but its own for a key scoped to a workspace — is a `404`, and so is `unassigned` from a scoped key, which cannot see those computers at all."}],"responses":{"200":{"description":"Your computers.","headers":{"X-GC-Incomplete":{"description":"Present only on a listing known to be short, which is answered only when `allow_partial` was passed: a host did not answer, or some rows were served from the platform’s own record rather than by their host. Its value is how many of the computers in the scope asked for no host answered for — the rows served from the platform’s own record, each carrying `unreachable: true` — counted before `state` filters the rows, so with `state=live` none of them is in the answer. It can be `0`, which means the listing may be short by computers the platform cannot name.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-GC-Incomplete":{"description":"Present when this `503` is the refusal of a short listing; pass `allow_partial=1` to be answered with the short listing instead. Its value is how many of the computers in the scope asked for no host answered for — the rows served from the platform’s own record, each carrying `unreachable: true` — counted before `state` filters the rows, so with `state=live` none of them is in the answer. It can be `0`, which means the listing may be short by computers the platform cannot name.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"post":{"summary":"Create a computer","description":"Builds a computer and, unless you pass `start: false`, starts it. The response carries the desktop credentials, so creating and connecting is one call. Every field has a default, so `{}` creates a computer from the default template; a request with no body at all is a `400`.\n\nA create that wants a running computer may be answered from the warm pool, in which case it returns in about the time the HTTP round trip takes. The pool stocks the shapes in `GET /sizes`, so naming a `size` is the likeliest way to get that answer. A cold create is slower (the guest is booted before the answer) and comes back `running`, or as `{computer, start_error}` with the computer stopped. On a host that is low on disk space, a create the warm pool would have answered is refused with `503` and nothing is created, while the pool there holds a ready computer of that shape the host has not paused, or holds only stock being held for disk-space recovery (that create answers the recovery `503`); it does not fall back to a cold create then. The host confirms it built nothing, so the attempt is not counted against your plan; a host that cannot confirm it, one running older software, leaves it counted (see the paragraph on a create whose outcome was not heard, below). Otherwise — the host has paused that stock, as it pauses all of it when space is critically low, or holds none — the create is built cold and its start is refused for the same lack of space: the answer is `201` with `{computer, start_error}`, and that computer exists, is stopped and counts against your plan. Either way no computer starts on that host until space there has been freed, so wait before sending it again rather than retrying it in a loop.\n\nA custom image whose verified holders have no capacity is copied on demand. During preparation, `409` with `code: template_image_preparing` guarantees no computer was created. Progress and errors are in `preparation`; repeat this request after `Retry-After` seconds with the returned `template_transfer` token to preserve the exact template and build across retries. Each retry rechecks quota and capacity. While the copy is under way it also carries `reason: \"contention\"`; a preparation that failed carries none, and `preparation.error` says why, unless what stopped it clears by itself (its source busy sending another image, no hypervisor yet with room for the image and the computer, or the destination already receiving it, full or not answering), when it carries `contention` too.\n\nA CREATE WHOSE OUTCOME WAS NOT HEARD STILL COUNTS AGAINST YOUR PLAN. A create, or a clone, that reached a host and whose answer never arrived, or was a `5xx` that cannot rule out a computer was built, is counted toward your plan’s limits from then on, with no time limit, until a listing of its host shows the computer it built. If it built nothing, it stays counted until that host is taken out of service or registered again. A host that refuses a create before building anything (a `503` from a host with no room left, for example) confirms it, and that attempt is not counted; one running older software cannot, and its `5xx` is counted like any other. `GET /account` does not include these, so a later `402` can name more than that read shows.\n\nThe computer goes in the workspace named by `workspace_id`, or, without it, in the API key’s: a key scoped to a workspace creates in that workspace, and an account-wide key or a dashboard session in none.\n\nA create’s response carries a `Server-Timing` header for diagnostics: `name;dur=<ms>` entries, the control plane’s first (`cp-total`, `cp-lane`, `cp-admit`, `cp-place`, `cp-forward`, and for a create bound to secrets `cp-start` and `cp-readback`), then the compute node’s (`total`, `decode`, `create`, `copy`, `start`, `admit`, `prepare`, `qemu`, `load`, `cont`, `identity`, with `identity` split into `id-thaw`, `id-clock`, `id-nic`, `id-script`, `id-password`, `id-link` and `id-screen`, and `deliver`). For a create bound to secrets the node’s `start` and the phases inside it come from the start that follows the create. A phase the create did not reach is left out. It is for diagnostics only: the set of names may change, so do not build logic on it.\n\nRequires the `member` role or higher.","operationId":"createComputer","tags":["Computers"],"x-rate-cost":100,"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Defaults to `computer`. Cut to 64 characters, with control characters turned into spaces and the ends trimmed. A name left empty by that gets the default too."},"workspace_id":{"type":"string","description":"The workspace to create the computer in — an id from `GET /workspaces`. Its `secrets` and an `egress_proxy`’s `credentials_secret_id` are then named from that workspace’s scope: its own secrets and the account-wide ones. Left out or `null`, the computer goes in the key’s workspace, or in none for an account-wide key. A workspace this key cannot reach — another account’s, one that does not exist, or any but its own for a key scoped to a workspace — is a `404`, and nothing is created."},"template_transfer":{"type":"string","description":"Token returned by a template_image_preparing 409. Retry the same create with this token after Retry-After seconds to preserve its template version and exact image. Progress and errors are returned in preparation. This is not a create idempotency key; stop retrying after success. A token that matches no preparation for this account and template is a 404."},"size":{"type":"string","description":"A named size from `GET /sizes` — the fast path, since these are the shapes hosts keep pre-booted. Sets the template and the three numbers together, so it cannot be combined with `template`, `cpu`, `ram_mb` or `disk_gb`."},"template":{"type":"string","description":"From `GET /templates` — either the short `name` or the pinned `ref` (`system/base@1.2.0`). Defaults to `base`.\n\nThe two differ in what happens when the name is wrong. A short name that is none of the templates we publish falls back to the default template — except while a host is missing from the catalogue, as `X-GC-Incomplete` on `GET /templates` shows. The name may then be one only that host has, so rather than build the default in its place the create is refused with a `503` before it is sent anywhere: nothing is done, no `operation_id` is named, and its `Idempotency-Key` is released. Send it again shortly. A `size` counts as the short name of its template here. A ref, a short name of one of the templates we publish that `GET /templates` still lists, and a create with neither `template` nor `size` are not affected by this. A ref that names nothing is refused with a `400`: one that is not a well-formed ref, or a version you retired, says so and lists nothing; a version that does not exist, of a name with versions your plan can launch, lists those versions; and any other lists the templates your plan can launch. The one exception is a `system/` ref while a host is missing from the catalogue: it may be one only that host has, so it is answered with a `503` instead, and is worth sending again shortly. If you would rather be told than guessed at, send the ref.\n\nA TEMPLATE YOU PUBLISHED IS NAMED BY ITS REF AND ONLY BY ITS REF. The short form belongs to the templates we publish, so `POST /templates` cannot change what an existing create builds. `namespace/name` without a version means the newest published version, resolved once — the computer is pinned to it, and publishing again afterwards does not reach back into a machine that is already building."},"cpu":{"type":"integer","description":"Cores. Defaults to the template’s. A whole number of at least 1, or the request is a `400` — except beside `size`, where `0` counts as left out.","minimum":0},"ram_mb":{"type":"integer","description":"Defaults to the template’s. A whole number of at least 1, or the request is a `400` — except beside `size`, where `0` counts as left out.","minimum":0},"disk_gb":{"type":"integer","description":"The template’s disk is a FLOOR, not a default: a smaller number is raised to it silently, and it is the raised figure your plan is charged for — so asking for less than the template needs can be refused 402 against a pool you did not think you were spending. A computer’s disk is an overlay over the template’s image and cannot be smaller than it. Read `disk_gb` on `GET /templates` to know the floor before you send one. A whole number of at least 1, or the request is a `400` — except beside `size`, where `0` counts as left out.","minimum":0},"start":{"type":"boolean","description":"Start it once built. Defaults to true."},"browser_proxy":{"$ref":"#/components/schemas/BrowserProxy","description":"Send this computer’s browsers through a proxy — see `browser_proxy` on the computer. Linux only; any other template is refused with 400, as is a value that fails the rules there, before anything is created. A create carrying one is always a cold boot, never answered from the warm pool or a boot capture, so that the policy is on disk before the desktop starts a browser; wait for `browser_proxy_pending` to clear before relying on it.\n\nA template you published can carry a default in `spec.browser_proxy`, and a create from it that leaves this field out gets that proxy, exactly as if it had been sent here. Send an object to use a different proxy instead, or `null` to create the computer with none. A create from such a template, whatever it sends here, answers 409 with `reason: \"unsupported\"` if the host it is placed on cannot launch one yet."},"egress_proxy":{"$ref":"#/components/schemas/EgressProxy","description":"Send ALL of this computer’s outbound TCP through a proxy — see `egress_proxy` on the computer. A value that fails the rules there is refused with 400 before anything is created. A create carrying one is never answered from the warm pool, whose computers have already been on the network, and is in effect from the first packet the computer sends. Answers 409 with `reason: \"unsupported\"` if the host it is placed on cannot take one yet — or cannot take an `https://` one, or one naming `credentials_secret_id`. A `credentials_secret_id` naming no secret in your scope, or one whose value is not `user:password`, is refused with 400 before anything is created; one whose value cannot be read on this control plane (replace its value), or named where delivering secrets is not switched on for the deployment, with 409; and where secrets are not enabled at all, with 503. Nothing is created for any of them."},"resolution":{"type":"string","description":"`WIDTHxHEIGHTxDEPTH` or `WIDTHxHEIGHT`, e.g. `1920x1080x24`: 640x480 to 3840x2160, both even, depth 16, 24 or 32 (24 when left out); anything else is a `400`. Defaults to `1280x800x24`. Create-time only — there is no route that changes it later."},"secrets":{"type":"array","description":"Secrets to deliver into the computer at cold start, each as an environment variable or as a file. A value replaced later reaches a running computer live when it is bound as a file. Bound as an environment variable, on an image that supports it, it reaches new shells within seconds, and every `exec` — a plain one, which runs as root, as well as one with `session: \"desktop\"` — runs with the bound environment variables as they are at that moment, so a replaced value applies to the next `exec`; programs already running keep the value they started with until a restart. Named by id from the account’s secrets; a create in a workspace — the one `workspace_id` names, or a scoped key’s own — may bind that workspace’s secrets, and every create may bind account-wide ones. Every start delivers each secret’s latest value. At most 32; Linux only, and only on a template whose image carries the delivery helper — any other is refused with 400 before anything is created. Started — the default — it is started with its secrets delivered, and the answer is the computer as that start left it, or `{computer, start_error}` when the start was refused.\n\nA computer bound to secrets suspends — by hand or when idle — and can be memory-snapshotted on any host that seals saved sessions: the image is encrypted on its way to disk with a key this API holds, and handed back only after checking the actor may still use every secret the image carries. One of those memory snapshots can be restored onto it, and it resumes holding what it held when the snapshot was taken (see `POST /snapshots/{id}/restore`). A DISK snapshot restored onto it starts it with its secrets delivered afresh, as a start does. Taking disk snapshots is allowed, and so is cloning — a clone copies the disk and comes out with no secrets bound, unless it is a fork of one of its memory snapshots made with `inherit_secrets` (see `POST /snapshots/{id}/clone`). It also moves between hosts: a move is cold, so no value travels. It lands stopped, and one you had running is started again for you once its secrets are delivered; one you had stopped stays stopped.\n\nBinding secrets has to be switched on for a deployment. Where it is not, a create carrying this field is refused with 400, and secrets can still be stored and managed with `/secrets` or under Settings. The condition is stated rather than the answer, because this text is fixed and the setting is not. Where binding is switched on but secrets are not enabled on the control plane, so their store cannot be opened, a create naming at least one secret here is refused with 503 once its entries have been checked, and nothing is created; there an empty list binds nothing, and the create goes ahead.","items":{"type":"object","properties":{"secret_id":{"type":"string","description":"The secret’s id, from `GET /secrets` or Settings. Each secret at most once."},"env":{"type":"string","description":"The environment variable the value is published under in the desktop session: letters, digits and underscores, not starting with a digit, at most 64 characters, each name at most once. Not a name the shell itself uses, which is refused with 400: bash special and startup variables such as `OPTIND`, `SECONDS`, `IFS`, `PS4`, `ENV` and `SHELLOPTS`, anything beginning `BASH_`, and anything beginning `__mandala_`."},"file":{"type":"string","description":"Instead of `env` (exactly one of the two): the file the value is published as, `/run/mandala-secrets/user/files/<file>`, readable by the desktop user and nobody else. Lowercase letters, digits, dashes and underscores, starting with a letter, at most 48 characters; at most 8 per computer. For what a program reads from a path — a kubeconfig, a key file. A file may hold any bytes, where an environment variable holds text. On some older images a list of files alone is refused with 400; bind at least one secret as `env` there."}},"required":["secret_id"]}}}}}}},"responses":{"201":{"description":"The computer, with its desktop credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResult"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"vm-a1b2c3d4e5f6","name":"scratch","status":"running","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"running_ram_mb":2048,"resolution":"1280x800x24","created_at":"2026-09-16T12:00:00.000Z","vnc":{"url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/vnc?token=illustrative-control-token","view_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/vnc?token=illustrative-view-token","token":"illustrative-control-token","view_token":"illustrative-view-token","terminal_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/terminal?token=illustrative-control-token","events_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/events?token=illustrative-control-token","embed_url":"https://app.mandala.computer/embed/desktop#computer=vm-a1b2c3d4e5f6&token=illustrative-view-token","clipboard":true},"operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso, with no `reason`, `Verify your email before launching a computer.`: the person this credential belongs to has not verified their email address. Nothing is created; verify it in the dashboard first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"template_transfer":{"type":"string","description":"With `code: \"template_image_preparing\"` only: the token to send back as `template_transfer` when you repeat this create after `Retry-After` seconds."},"preparation":{"type":"object","properties":{"state":{"type":"string","enum":["preparing","copying","ready","failed"],"description":"How far the copy of the template’s image has got."},"error":{"type":["string","null"],"description":"Why the preparation failed, or `null` while it has not."}},"required":["state","error"]}}}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"scratch\",\"template\":\"base\",\"resolution\":\"1920x1080x24\",\"start\":true}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    body = json.loads(\"{\\\"name\\\":\\\"scratch\\\",\\\"template\\\":\\\"base\\\",\\\"resolution\\\":\\\"1920x1080x24\\\",\\\"start\\\":true}\")\n    result = client.computers.create(**body)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst body = {\"name\":\"scratch\",\"template\":\"base\",\"resolution\":\"1920x1080x24\",\"start\":true};\nconst result = await client.computers.create(body);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputers","tags":["Computers"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}":{"get":{"summary":"Get a computer","description":"One computer, with the credentials to open its desktop.\n\nRequires the `viewer` role or higher.","operationId":"getComputer","tags":["Computers"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The computer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputerConnect"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"vm-a1b2c3d4e5f6","name":"scratch","status":"running","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"running_ram_mb":2048,"resolution":"1280x800x24","created_at":"2026-09-16T12:00:00.000Z","vnc":{"url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/vnc?token=illustrative-control-token","view_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/vnc?token=illustrative-view-token","token":"illustrative-control-token","view_token":"illustrative-view-token","terminal_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/terminal?token=illustrative-control-token","events_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/events?token=illustrative-control-token","embed_url":"https://app.mandala.computer/embed/desktop#computer=vm-a1b2c3d4e5f6&token=illustrative-view-token","clipboard":true}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    result = client.computers.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst result = await client.computers.get(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get a computer (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetComputer","tags":["Computers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The computer.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"patch":{"summary":"Update a computer","description":"To change `workspace_id`, use `PUT /computers/{id}/workspace`. Sending it here is a `400`, even alongside other fields; nothing is changed.\n\nRename it, resize it, change how long it may idle before its host suspends it, or set its browser proxy or its egress proxy. A request with no body is a `400`. So is one naming `size`: a resize takes explicit `cpu`, `ram_mb` and `disk_gb`, which are on the row in `GET /sizes`.\n\nSEND ONE OF THOSE FIVE AT A TIME. `name`, the sizing group (`cpu`/`ram_mb`/`disk_gb`), `idle_suspend_min`, `browser_proxy` and `egress_proxy` each refuse to travel with any of the others, and a request carrying two is a 400. They are separated because a resize needs the computer stopped and the others do not, so one request could not honour both without silently applying half of it.\n\nA `browser_proxy` sent to a running computer is written into it within seconds (watch `browser_proxy_pending`); sent to a stopped or suspended one it is stored and applied as it starts. One sent to a host that cannot set a browser proxy yet — a `null` included — answers 409 with `reason: \"unsupported\"`, as does a setting naming `credentials_secret_id` sent to a host that cannot give a browser proxy credentials yet; a host that could not be asked which it can do answers 503. Nothing is changed by any of these: send a 409 again once the host has been updated, and the 503 shortly.\n\nAn `egress_proxy` sent to a running computer is in effect when the answer arrives, and the connections it had open are closed; sent to a stopped or suspended one it is stored and applied before it starts. One its host cannot put into effect is refused with 503 and nothing is changed — unless the computer is being moved to another host at that moment, when the previous setting cannot be put back: then the 503 says so, the new setting is stored, and it takes effect at the host’s next successful write of its egress rules, retried every few seconds; until then the computer’s connections may go directly, or through its previous proxy if it had one. A setting sent to a host that cannot proxy a computer yet answers 409 with `reason: \"unsupported\"`, as does an `https://` setting, or one naming `credentials_secret_id`, sent to a host that cannot take those yet; a `null` is refused only by a host that does not know the field at all, so an egress proxy can always be removed. A `credentials_secret_id` naming no secret in the computer’s scope, or one whose value is not `user:password`, is refused with 400 and nothing is changed; so, with 409, is one whose value cannot be read on this control plane (replace its value) or one named where delivering secrets is not switched on for the deployment, and, with 503, one named where secrets are not enabled at all. A setting naming it is in effect once the host has the value, usually within seconds (watch `egress_proxy_pending`), and every connection is closed until then. A `null`, or a setting naming no `credentials_secret_id` or a different one, that its host answers with a 5xx, or that is sent and never answered, may or may not have taken effect, and the host is also told to forget the credentials it held for the secret the previous setting named. (A 503 whose error says nothing was changed was answered before the request was sent: it changed nothing, and the host keeps those credentials.) If the setting in effect afterwards names a `credentials_secret_id` — the previous one, once the host has forgotten its credentials, or the new one, whose value is not delivered after such an answer — a running computer reports `egress_proxy_pending` and its connections are closed until the setting is sent again, and a stopped or suspended one starts that way; if it names none, it behaves as described above.\n\nA resize of a RUNNING computer answers 409 with `reason: \"running\"`: stop it and send the same request again. Waiting does not clear it, and neither does a start.\n\nA RESIZE DISCARDS A SAVED SESSION. Suspended counts as stopped here, so a resize of a suspended computer is accepted — and the saved desktop cannot survive it, because the vCPU count and the memory size are part of the state and growing the disk changes a device the state describes. It is dropped, and the next start is a cold boot. Resume it and finish what is open before resizing, or accept the loss deliberately.\n\nA memory snapshot taken before a resize cannot be restored afterwards either — see `POST /snapshots/{id}/restore`.\n\nResolution is not settable — it is fixed at create.\n\nRequires the `member` role or higher.","operationId":"updateComputer","tags":["Computers"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"On its own — not alongside a resize, `idle_suspend_min`, `browser_proxy` or `egress_proxy`. Cut to 64 characters, with control characters turned into spaces and the ends trimmed; a name that is empty after that is a 400 (`name required`)."},"cpu":{"type":"integer","description":"Requires the computer to be stopped. Send with the rest of the sizing group only."},"ram_mb":{"type":"integer","description":"Requires the computer to be stopped."},"disk_gb":{"type":"integer","description":"Grow only. Requires the computer to be stopped."},"idle_suspend_min":{"type":["integer","null"],"description":"Minutes, and it must be the ONLY field in the request. `null` clears the override and returns this computer to its host’s sweep. At most 10080 (a week); a negative number or more is a `400`. Zero disables idle suspend and pressure eviction, subject to the account’s never-suspend limit (Solo 0, Studio 1, Swarm 4); adding a pin beyond it returns 402. Existing pins are preserved."},"browser_proxy":{"oneOf":[{"$ref":"#/components/schemas/BrowserProxy","description":"On its own. Replaces this computer’s browser proxy whole; `null` removes it, and its files are taken out of the computer. A Windows computer is refused with 400, as is a value that fails the rules on `browser_proxy`."},{"type":"null"}],"description":"On its own. Replaces this computer’s browser proxy whole; `null` removes it, and its files are taken out of the computer. A Windows computer is refused with 400, as is a value that fails the rules on `browser_proxy`."},"egress_proxy":{"oneOf":[{"$ref":"#/components/schemas/EgressProxy","description":"On its own. Replaces this computer’s egress proxy whole — `server` and `credentials_secret_id` together, so a setting sent without the id removes the credentials; `null` removes it, and the computer’s traffic goes directly again. A value that fails the rules on `egress_proxy` is refused with 400."},{"type":"null"}],"description":"On its own. Replaces this computer’s egress proxy whole — `server` and `credentials_secret_id` together, so a setting sent without the id removes the credentials; `null` removes it, and the computer’s traffic goes directly again. A value that fails the rules on `egress_proxy` is refused with 400."}}}}}},"responses":{"200":{"description":"The updated computer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputerConnect"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"vm-a1b2c3d4e5f6","name":"renamed","status":"running","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"resolution":"1280x800x24","created_at":"2026-09-16T12:00:00.000Z","vnc":{"url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/vnc?token=illustrative-control-token","view_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/vnc?token=illustrative-view-token","token":"illustrative-control-token","view_token":"illustrative-view-token","terminal_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/terminal?token=illustrative-control-token","events_url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/events?token=illustrative-control-token","embed_url":"https://app.mandala.computer/embed/desktop#computer=vm-a1b2c3d4e5f6&token=illustrative-view-token","clipboard":true}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.\n\nAlso `reason: \"contention\"` for an `egress_proxy` naming `credentials_secret_id` that reached the host after the computer moved to another workspace: nothing was changed, and sent again it is checked against the workspace the computer is in now.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"move":{"type":"object","properties":{"required":{"type":"boolean","const":true,"description":"Always `true`: this size needs the computer on another host."},"possible":{"type":"boolean","description":"`true` when another host in this region can run it — agree with `POST /computers/{id}/move` and the same sizing fields; `false` when nowhere in the region can, and the size is what to change."}},"required":["required","possible"]}}}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PATCH \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"renamed\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"name\\\":\\\"renamed\\\"}\")\n    c = client.computers.get(id)\n    result = c.rename(body[\"name\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"name\":\"renamed\"};\nconst c = await client.computers.get(id);\nconst result = await c.update(body);\nconsole.log(JSON.stringify(result, null, 2));"}]},"delete":{"summary":"Delete a computer","description":"Destroys the computer, keeping snapshots by default. With `snapshots=delete`, deletes the computer while keeping its snapshots, then deletes only the selected physical snapshot copies across the fleet. Copies with the same ID on different hosts count separately.\n\nRead `GET /computers/{id}/snapshots` and pass its `fingerprint` as `expect`. A stale fingerprint returns 409 before any mutation. If expect is omitted, a fresh complete inventory defines the selected copies for this explicit purge request. Unavailable inventory or an active capture prevents purge; ordinary deletion keeping snapshots remains available.\n\nAn active capture, and a computer being moved between hosts, answer 409 with `reason: \"contention\"`: send the same request again once it finishes (with `expect`, a capture that has finished changes the fingerprint, so read the holdings again). A stale fingerprint, a changed fleet and changed ownership carry no `reason`. A host that refuses the computer’s own delete passes its `reason` on — `contention` while another delete, or a copy of its disk, is already running.\n\nInspect `computer_deleted` and `purge` even on success: 202 means work remains queued. Partial refusals return 409; unknown transport or incomplete final inventory returns 503. A computer already deleted before this request, whose purge deleted no copy and was refused only by copies still in use (a clone reading one, say), carries `reason: \"contention\"` when it was sent with `expect`: send the same request again once they are free, and any copy that appeared since is refused before anything is deleted. No uncertain delete is replayed automatically. Refresh holdings before an explicit retry, including after the computer is gone. The final inventory reports new unselected copies; those need new consent. Completion covers the selected copies as observed, not a guarantee that no later import can appear.\n\nRequires the `member` role or higher.","operationId":"deleteComputer","tags":["Computers"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"snapshots","in":"query","required":false,"schema":{"type":"string","enum":["delete"]},"description":"Delete this computer’s snapshots along with it. Omit to keep them."},{"name":"expect","in":"query","required":false,"schema":{"type":"string"},"description":"The `fingerprint` from `GET /computers/{id}/snapshots`. Makes the purge binding on the set you were shown. Ignored unless `snapshots=delete`."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"responses":{"200":{"description":"Computer deletion and physical-copy cleanup outcomes; 202 is pending, not completion.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResult"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"snapshots_deleted":0,"operation_id":"op_a1b2c3d4e5f60718293a4b5c"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"202":{"description":"Computer deletion and physical-copy cleanup outcomes; 202 is pending, not completion.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResult"},"examples":{"queued":{"summary":"Computer deletion is confirmed; a selected snapshot copy is still queued for deletion.","value":{"ok":false,"computer_deleted":true,"snapshots_deleted":0,"purge":{"selected":1,"confirmed":0,"queued":1,"failed":0,"unknown":0,"remaining":1,"unselected":0,"complete":false},"error":"The computer was deleted. Snapshot deletion is queued; review the remaining copies.","operation_id":"op_a1b2c3d4e5f60718293a4b5c"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise. On this status, only a delete whose answer said the computer was deleted (`computer_deleted: true`, or a `purge` result) is kept and replayed.","schema":{"type":"string","enum":["true"]}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise. On this status, only a delete whose answer said the computer was deleted (`computer_deleted: true`, or a `purge` result) is kept and replayed.","schema":{"type":"string","enum":["true"]}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise. On this status, only a delete whose answer said the computer was deleted (`computer_deleted: true`, or a `purge` result) is kept and replayed.","schema":{"type":"string","enum":["true"]}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise. On this status, only a delete whose answer said the computer was deleted (`computer_deleted: true`, or a `purge` result) is kept and replayed.","schema":{"type":"string","enum":["true"]}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"ok":{"type":"boolean","description":"Whether the requested selected-copy cleanup completed."},"snapshots_deleted":{"type":"integer","description":"Physical snapshot copies confirmed deleted. Duplicate IDs on different hosts count separately. Zero when snapshots are kept."},"computer_deleted":{"type":["boolean","null"],"description":"For fleet purge requests: true records a prior confirmed computer deletion or complete fleet absence observed during this request; it does not guarantee that the computer remains absent. false means deletion is unconfirmed or the computer was observed present again. null means the deletion outcome or current presence is unknown, including transport uncertainty or a fleet topology change."},"error":{"type":"string","description":"An explanation of a partial, queued, refused, or unknown outcome."},"purge":{"$ref":"#/components/schemas/SnapshotPurge"},"operation_id":{"type":"string","description":"On a keyed delete: this delete’s operation. `succeeded` when the answer says the computer was deleted (`computer_deleted: true`, or a `purge` beside `false`): the key keeps this answer and replays it. `pending` when the outcome is unknown (`computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place): the key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. Absent when nothing happened and the key was released, including the `contention` refusal that deleted, queued and left in doubt nothing. Read `GET /operations/{id}` for its state."}}}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise. On this status, only a delete whose answer said the computer was deleted (`computer_deleted: true`, or a `purge` result) is kept and replayed.","schema":{"type":"string","enum":["true"]}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise. On this status, only a delete whose answer said the computer was deleted (`computer_deleted: true`, or a `purge` result) is kept and replayed.","schema":{"type":"string","enum":["true"]}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise. On this status, only a delete whose answer said the computer was deleted (`computer_deleted: true`, or a `purge` result) is kept and replayed.","schema":{"type":"string","enum":["true"]}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"ok":{"type":"boolean","description":"Whether the requested selected-copy cleanup completed."},"snapshots_deleted":{"type":"integer","description":"Physical snapshot copies confirmed deleted. Duplicate IDs on different hosts count separately. Zero when snapshots are kept."},"computer_deleted":{"type":["boolean","null"],"description":"For fleet purge requests: true records a prior confirmed computer deletion or complete fleet absence observed during this request; it does not guarantee that the computer remains absent. false means deletion is unconfirmed or the computer was observed present again. null means the deletion outcome or current presence is unknown, including transport uncertainty or a fleet topology change."},"error":{"type":"string","description":"An explanation of a partial, queued, refused, or unknown outcome."},"purge":{"$ref":"#/components/schemas/SnapshotPurge"},"operation_id":{"type":"string","description":"On a keyed delete whose outcome is unknown: this delete’s operation, left `pending` even when `computer_deleted` is `true`, because the snapshot cleanup’s outcome is unknown. The same key answers `409 idempotency_outcome_unknown` from now on: read the computer, its snapshots and `GET /operations/{id}` rather than resending. Absent on a `503` refused before it was sent anywhere, whose key was released."}}}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.delete()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.delete();\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersById","tags":["Computers"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/start":{"post":{"summary":"Start a computer","description":"Boots a stopped computer, or resumes a suspended one. A cold start that would deliver secrets answers 409 while a workspace change is pending or the recorded binding scope disagrees with the computer’s current workspace. Check the workspace change in the dashboard. For conflicting bindings, remove them with `PUT /computers/{id}/secrets` and `{\"secrets\":[]}`, check any pending workspace change again, then retry with no bindings or bind secrets in the current workspace.\n\nA computer the platform is holding for disk-space recovery, after a suspend or memory snapshot failed (at any free space; see `POST /computers/{id}/suspend`), answers `503` (“waiting for disk-space recovery”) until it resumes by itself once that recovery can finish. Stop it to discard the paused session instead; one that already reads `stopped` will not resume by itself, so stop it.\n\nOn a host that is low on disk space a start or resume answers `503` (“this host is low on disk space; this computer cannot start or resume until space has been freed”) and starts nothing; a suspended computer keeps its saved session. It is refused the same way until space on that host has been freed, so sending it again soon does not help. One saying the host’s disk space “cannot be checked right now” is a failed reading, and can be sent again shortly. If the message adds that its paused QEMU could not be stopped, stop the computer before trying again.\n\nRequires the `member` role or higher.","operationId":"startComputer","tags":["Computers"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"resume_only","in":"query","required":false,"schema":{"type":"string","enum":["true"]},"description":"Resume only if a saved session still exists. A stopped computer is not booted and the call succeeds, but it is checked against your plan first like any start, so it can answer `402`, or `503` when your plan usage cannot be checked."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"responses":{"200":{"description":"Started, or no saved session remains when resume_only=true.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LifecycleAck"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.\n\nAlso the secret workspace refusals of a cold start, answered before anything starts: `reason: \"contention\"` while the computer is being moved to another workspace, which clears in moments, and `reason: \"unsupported\"` for a workspace change whose outcome was lost (check it in the dashboard) or recorded bindings that belong to a workspace the computer is no longer in, neither of which waiting clears (see the recovery above).\n\nAnd the rest of a cold start’s secret refusals, also before anything starts. Its host holding a list of bindings that does not match the recorded one is `reason: \"contention\"` while a change to its secrets is still being saved or has just landed, and `reason: \"unsupported\"` otherwise: save the list again with `PUT /computers/{id}/secrets`. Such a start asks the host again before answering, and a host that cannot be asked again answers `503`; send it again shortly. A bound secret deleted or out of the computer’s scope, one whose value cannot be read here or is not text an environment variable can carry, and secrets delivery not switched on for this deployment are each `reason: \"unsupported\"`: the sentence names the fix, and none of them clears by waiting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso a host that did not answer in time, whose error says the request may still have been carried out: the start may still have gone ahead. Read the computer’s status before retrying. That answer names the start’s `operation_id`: sent with an `Idempotency-Key`, that operation stays `pending` and resending under the same key answers `idempotency_outcome_unknown` instead of carrying it out again; sent without one, it is recorded `failed` with `code: \"lost\"`.\n\nAlso a sealed session the platform could not open, seal or carry just then — the service that holds its key did not answer, or the host failed moving the saved session — which is the platform’s fault and not the request’s. Read the computer’s status, then send the start again; it usually clears in a moment. Sent with an `Idempotency-Key`, sending it again under the same key is safe: when the host refused it before changing anything, its key was released and the answer names no `operation_id`, so the resend is carried out; when the answer names an `operation_id`, the outcome is unknown and the resend answers `409 idempotency_outcome_unknown` — read the computer and that operation instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/start' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.start()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.start();\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdStart","tags":["Computers"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/stop":{"post":{"summary":"Stop a computer","description":"Powers it off. The disk is kept; you stop paying for the memory. On a suspended computer this discards the saved session, and the next start is a fresh boot.\n\nThe guest is asked to shut down and given time to do it. A graceful stop waits up to 20 seconds for the guest, then forces it off, so the call can take up to about a minute to answer. `force=true` skips the asking and pulls the power, which is the equivalent of holding the button in: it is what to reach for when a guest will not come down on its own, and it can lose whatever had not been written to disk.\n\nRequires the `member` role or higher.","operationId":"stopComputer","tags":["Computers"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"force","in":"query","required":false,"schema":{"type":"string","enum":["true"]},"description":"Pull the power instead of asking the guest to shut down. Unwritten data is lost."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"responses":{"200":{"description":"Stopped.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LifecycleAck"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso a host that did not answer in time, whose error says the request may still have been carried out: the stop may still have gone ahead. Read the computer’s status before retrying. That answer names the stop’s `operation_id`: sent with an `Idempotency-Key`, that operation stays `pending` and resending under the same key answers `idempotency_outcome_unknown` instead of carrying it out again; sent without one, it is recorded `failed` with `code: \"lost\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/stop' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.stop()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.stop();\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdStop","tags":["Computers"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/suspend":{"post":{"summary":"Suspend a computer","description":"Saves the running session to disk and gives the host its memory back, without ending the session. Starting it again resumes the desktop exactly where it was. The guest’s clock is set to the current time within a few seconds of the resume, through its guest agent — best effort, so a guest whose agent does not answer keeps the time it was suspended at until NTP corrects it — and the desktop panel’s clock can show the old time for up to a minute after that. A stopped computer answers `409` with `reason: \"unavailable\"`: start it before suspending a session.\n\nA computer that holds secrets suspends only on a host that seals saved sessions: its memory is encrypted on its way to disk. Elsewhere, and while its secrets are still being delivered, it answers `409`: with `reason: \"contention\"` while a delivery is on its way, which passes in a moment; with `reason: \"unsupported\"` for a session its secrets were never delivered to and that nothing is delivering to now, which only a stop and a start clears; and with no `reason` when its guest did not say whether its secrets reached the boot it is running. For a session its secrets were never delivered to, it also answers `409` with `reason: \"contention\"` when its host could not read whether it is running just now (send it again shortly), and with `reason: \"unsupported\"` when its host cannot read its process until support repairs the host. One whose bindings dropped a secret or moved one to another revision while it ran cannot be suspended until it restarts, because its memory may still hold the old value.\n\nA refusal known to occur before this suspend attempt starts saving includes `suspend_outcome: \"not_started\"`. Without this field, a failed request may have saved a session: refresh status before further lifecycle actions. HTTP `400` or `409` alone does not establish whether saving began.\n\nA suspend that fails, especially while the host is low on disk space, can leave the computer paused, still reading `running`. If start, restart, suspend or snapshot then answer `503` (“waiting for disk-space recovery”), the platform is holding it for recovery: it resumes by itself once that recovery can finish, or stop it to discard the paused session. A failure saying “this computer remains paused because the host is low on disk space” is always held that way, and so can other failures be, at any free space. A suspend sent again that is not refused that way goes ahead, and it completes a session that was saved but could not be recorded rather than saving a second one.\n\nRequires the `member` role or higher.","operationId":"suspendComputer","tags":["Computers"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"responses":{"200":{"description":"Suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LifecycleAck"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"suspend_outcome":{"type":"string","enum":["not_started"],"description":"Present only when this suspend was refused before it began saving the session. Absent, the request may have saved one: read the computer before another lifecycle action."}}}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"suspend_outcome":{"type":"string","enum":["not_started"],"description":"Present only when this suspend was refused before it began saving the session. Absent, the request may have saved one: read the computer before another lifecycle action."}}}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso a host that did not answer in time, whose error says the request may still have been carried out: the suspend may still have gone ahead. Read the computer’s status before retrying. That answer names the suspend’s `operation_id`: sent with an `Idempotency-Key`, that operation stays `pending` and resending under the same key answers `idempotency_outcome_unknown` instead of carrying it out again; sent without one, it is recorded `failed` with `code: \"lost\"`.\n\nAlso a sealed session the platform could not open, seal or carry just then — the service that holds its key did not answer, or the host failed moving the saved session — which is the platform’s fault and not the request’s. Read the computer’s status, then send the suspend again; it usually clears in a moment. Sent with an `Idempotency-Key`, sending it again under the same key is safe: when the host refused it before changing anything, its key was released and the answer names no `operation_id`, so the resend is carried out; when the answer names an `operation_id`, the outcome is unknown and the resend answers `409 idempotency_outcome_unknown` — read the computer and that operation instead.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"suspend_outcome":{"type":"string","enum":["not_started"],"description":"Present only when this suspend was refused before it began saving the session. Absent, the request may have saved one: read the computer before another lifecycle action."}}}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/suspend' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.suspend()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.suspend();\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdSuspend","tags":["Computers"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/restart":{"post":{"summary":"Restart a computer","description":"Resets the guest — the equivalent of the reset button, not a fresh boot of a new machine. Changes that need a different QEMU command line, such as a resize, need a stop and a start instead. A stopped computer with no secrets bound is started instead, and that start can be refused like any other, including with `503` while its host is low on disk space (see `POST /computers/{id}/start`); a suspended one answers 409 — start it to resume, or stop it to discard the session.\n\nA restart issues new desktop credentials and closes open desktop and terminal connections, so a `vnc.url` from before it stops working: read the computer again for a fresh one.\n\nA computer that holds secrets gets them again as it comes back up, on any host that seals saved sessions. It answers 409 when it is stopped, with `reason: unavailable` — start it instead, which delivers them — while its secrets are still being delivered, with `reason: contention`, which passes in a moment, and on a host that cannot deliver them again, with `reason: unsupported`, where a stop and a start is the way to restart it — as it is for a session its secrets were never delivered to and that nothing is delivering to now, also `reason: unsupported`. For such a session, a host that could not read whether it is running just now answers `reason: contention` (send it again shortly), and one that cannot read its process until support repairs the host answers `reason: unsupported`. After a restart, `secrets_delivering` reads `true` before the computer is back up; a restart is still accepted then, for a computer that does not come back, and is refused with `reason: contention` only once the delivery has begun.\n\nA computer the platform is holding for disk-space recovery, after a suspend or memory snapshot failed at any free space (see `POST /computers/{id}/suspend`), answers `503` (“waiting for disk-space recovery”), running or stopped, before anything is reset or any credential changes. Wait for it to resume by itself, or stop it to discard the paused session. One that is paused because its host ran out of disk space, still reading `running`, answers `503` too, saying so, and keeps its session: it resumes by itself once space is freed, so restart it then, or stop it. One its host paused while low on disk space is restarted, and comes back once space has been freed. A restart that finds another operation holding the computer’s monitor answers `409` with `reason: contention` and changes nothing; send it again.\n\nRequires the `member` role or higher.","operationId":"restartComputer","tags":["Computers"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"responses":{"200":{"description":"Restarting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LifecycleAck"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso a host that did not answer in time, whose error says the request may still have been carried out: the restart may still have gone ahead. Read the computer’s status before retrying. That answer names the restart’s `operation_id`: sent with an `Idempotency-Key`, that operation stays `pending` and resending under the same key answers `idempotency_outcome_unknown` instead of carrying it out again; sent without one, it is recorded `failed` with `code: \"lost\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/restart' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.restart()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.restart();\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdRestart","tags":["Computers"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/workspace":{"put":{"summary":"Move a computer to a workspace","description":"Move to another workspace on this account, or send `workspace_id: null` to leave it unassigned. Requires a member or owner using an account-wide credential. Every workspace-confined API key or OAuth credential is refused with `403`, including its own workspace, no-op moves and unassignment.\n\nThe body must contain exactly `workspace_id`: a `wsp-` ID followed by 12 lowercase hexadecimal characters, or `null`. Missing, empty, malformed or extra fields are refused; the body is capped at 1024 bytes. An unknown workspace or one in another account answers `404`.\n\nThe computer must be stopped with no saved session, no snapshots on any host, no secret bindings and no proxy credentials. Nothing is automatically stopped, discarded, unbound or deleted. Snapshot history and unresolved migration copies must be verifiable across the fleet; offline history without durable proof needs operator recovery. A verified no-op returns the current public computer.\n\nOnce moved, old workspace credentials lose access and destination credentials gain access. Events and webhooks follow the new workspace; desktop and share credentials are replaced. The response contains the public Computer only, without desktop credentials or internal move state.\n\nIf the outcome is uncertain, repeating the same PUT reconciles the pending change before any further move. The refusal explains whether to retry or resolve snapshot, migration or binding state first. This route has no Idempotency-Key contract and accepts no internal request ID, proof, lease or repair fields.\n\nRequires the `member` role or higher.","operationId":"moveComputerToWorkspace","tags":["Computers"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspace_id":{"type":["string","null"],"description":"Destination workspace ID, or null for unassigned.","pattern":"^wsp-[a-f0-9]{12}$"}},"required":["workspace_id"],"additionalProperties":false}}}},"responses":{"200":{"description":"The computer in its confirmed workspace.","content":{"application/json":{"schema":{"type":"object","properties":{"computer":{"$ref":"#/components/schemas/Computer"}},"required":["computer"]},"examples":{"success":{"summary":"The public computer in the confirmed destination workspace.","value":{"computer":{"id":"vm-a1b2c3d4e5f6","name":"scratch","status":"stopped","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"running_ram_mb":2048,"resolution":"1280x800x24","created_at":"2026-09-16T12:00:00.000Z","state":"live","workspace_id":"wsp-a1b2c3d4e5f6"}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nThe body is not the required object containing only workspace_id. The sentence names what is wrong: a field other than workspace_id, a missing workspace_id, or one that is not a workspace ID or null.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer to a credential confined to a workspace, with no reason.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.\n\nThe computer or destination workspace does not belong to this account, or does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nThe computer has a saved session, is not stopped, has snapshots, bindings or proxy credentials, or conflicting move state needs recovery. A computer that is not stopped (running or suspended, say) or has a saved session is refused with `reason: \"unsupported\"`: waiting does not clear it; stop it and discard any saved session first. A stopped computer whose host has not yet settled its state gets the same answer, and that one can clear by itself: if the computer is already stopped with no saved session, send the request again shortly. With `reason: \"contention\"` instead when it clears by itself: its disk is still being made, it started moving between hosts or changed while it was being checked, or its host is busy with other work on it. Send the same request again shortly. With `reason: \"unsupported\"` when a host that may hold this account’s snapshot history was taken offline without verifying it: waiting does not clear it, and an operator must recover or retire that host’s snapshot history first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThe body exceeds 1024 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nThe account lane is busy, a running host cannot provide verifiable snapshot history or migration copies, snapshot storage is unavailable, a snapshot transfer is pending, snapshot history or the fleet changed during the checks, or the move outcome is uncertain. Follow the refusal before retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PUT \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/workspace' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"workspace_id\":\"wsp-a1b2c3d4e5f6\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"workspace_id\\\":\\\"wsp-a1b2c3d4e5f6\\\"}\")\n    c = client.computers.get(id)\n    result = c.move_to_workspace(body[\"workspace_id\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"workspace_id\":\"wsp-a1b2c3d4e5f6\"};\nconst c = await client.computers.get(id);\nconst result = await c.moveToWorkspace(body.workspace_id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdWorkspace","tags":["Computers"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/secrets":{"get":{"summary":"Read the secrets bound to a computer","description":"Which secrets this computer is bound to, the revision of each and the environment variable or file it is published under, and the list's `version`, which a change sends back to say which list it is changing.\n\nA list this platform cannot read is answered as `secrets: []` with its `version`, though the computer is still bound. Any change to it, `{\"secrets\":[]}` included, is refused with `409` and `reason: \"unsupported\"`; contact support.\n\nRequires the `viewer` role or higher.","operationId":"getComputerSecrets","tags":["Computers"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The bindings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBindings"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"secrets":[{"secret_id":"csec-0123456789abcdef","revision_id":"csr-0123456789abcdef01234567","env":"OPENAI_API_KEY"}],"version":1}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nThe workspace change is pending, or the recorded binding scope disagrees with the computer’s current workspace; no binding list is returned. A move being made right now carries `reason: \"contention\"` and clears in moments. A change whose outcome was lost, and a disagreeing scope, carry `reason: \"unsupported\"`: neither clears by waiting. Check the workspace change in the dashboard, which settles a lost one. For conflicting bindings, send `{\"secrets\":[]}` to `PUT /computers/{id}/secrets` without a `version`, then check any pending workspace change again before binding secrets in the current workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/secrets' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.secrets()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.secrets();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read the secrets bound to a computer (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetComputerSecrets","tags":["Computers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The bindings.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nThe workspace change is pending, or the recorded binding scope disagrees with the computer’s current workspace; no binding list is returned. A move being made right now carries `reason: \"contention\"` and clears in moments. A change whose outcome was lost, and a disagreeing scope, carry `reason: \"unsupported\"`: neither clears by waiting. Check the workspace change in the dashboard, which settles a lost one. For conflicting bindings, send `{\"secrets\":[]}` to `PUT /computers/{id}/secrets` without a `version`, then check any pending workspace change again before binding secrets in the current workspace.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"put":{"summary":"Change the secrets bound to a computer","description":"Replaces the whole list; `[]` removes every binding. Each secret is resolved in the computer's own scope, as at create, and recorded at its LATEST revision — unless the entry names the `revision_id` it holds now, which keeps that one. Every start and restart delivers the latest value of each bound secret and moves its `revision_id` to it, so a replaced value needs at most a restart — one bound as a file, or as an environment variable on an image that supports it, is also sent to a running computer when it is replaced (see `PUT /secrets/{id}`), and moves `revision_id` once that lands. A binding says what was last delivered to the computer — or, until anything has been, the revision it was bound at — not what it is held to.\n\nTHE NEW VALUES ARRIVE AT THE NEXT START OR RESTART. A running computer keeps the environment it was started with until then, and a suspended one resumes holding the values it had when it was suspended. On an image that supports it, what starts AFTER the change is the exception: new shells and every `exec` on a running computer — plain or with `session: \"desktop\"` — see the new environment variables within seconds, with a removed one unset, while programs already running keep what they started with until the restart. A file newly bound or renamed waits for the restart either way. Restoring an older memory snapshot brings back that snapshot's values until the next reboot. A running computer whose change drops a secret or moves one to another revision cannot be suspended or memory-snapshotted until it restarts, because its memory may still hold the old value. Binding a computer for the first time needs a Linux template whose image can receive secrets, as create does, and the computer STOPPED: one started without secrets cannot receive them until it starts again, and a restart does not do that. 409 while it is running or suspended.\n\nSend the `version` a read answered to change only the list you read: 409 if it has changed since. It answers 409 as well while a delivery is still in progress, and for a list that no longer binds, as a file, the secret the computer’s `browser_proxy.credentials_secret_id` names. It can also answer 409 during another operation, such as a stop, snapshot or suspend; retry once that operation finishes.\n\nA nonempty list answers 409 while a workspace change is pending or the recorded binding scope disagrees with the computer’s current workspace. Before dispatch, a workspace change after binding resolution answers 409 if the computer remains visible to you; a scoped credential that loses access gets 404. An already accepted request can still answer 409 if the workspace changes while its reply is pending, even if that credential has lost access. Check the workspace change in the dashboard. For conflicting bindings, removing the entire list with `{\"secrets\":[]}` remains available wherever binding secrets is switched on (where it is not, every change to the list is a `400`); omit `version` if reading the list answers 409. Then check any pending workspace change again before binding secrets in the current workspace.\n\nRequires the `member` role or higher.","operationId":"setComputerSecrets","tags":["Computers"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"secrets":{"type":"array","description":"The whole list. At most 32.","items":{"type":"object","properties":{"secret_id":{"type":"string","description":"The secret’s id, from `GET /secrets` or Settings. Each secret at most once."},"env":{"type":"string","description":"The environment variable the value is published under in the desktop session: letters, digits and underscores, not starting with a digit, at most 64 characters, each name at most once. Not a name the shell itself uses, which is refused with 400: bash special and startup variables such as `OPTIND`, `SECONDS`, `IFS`, `PS4`, `ENV` and `SHELLOPTS`, anything beginning `BASH_`, and anything beginning `__mandala_`."},"file":{"type":"string","description":"Instead of `env` (exactly one of the two): the file the value is published as, `/run/mandala-secrets/user/files/<file>`, readable by the desktop user and nobody else. Lowercase letters, digits, dashes and underscores, starting with a letter, at most 48 characters; at most 8 per computer. For what a program reads from a path — a kubeconfig, a key file. A file may hold any bytes, where an environment variable holds text. On some older images a list of files alone is refused with 400; bind at least one secret as `env` there."},"revision_id":{"type":"string","description":"Optional: the revision this computer holds now, to keep it. Omit to record the latest. Any other revision, including one it held before, is refused with `400` and nothing is changed."}},"required":["secret_id"]}},"version":{"type":"integer","description":"The `version` a read of this list answered. Omit to change it whatever it is."}},"required":["secrets"]}}}},"responses":{"200":{"description":"The bindings as they now are.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBindings"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"secrets":[{"secret_id":"csec-0123456789abcdef","revision_id":"csr-89abcdef0123456789abcdef","env":"OPENAI_API_KEY"}],"version":2}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\n`reason` says which of these clear. `contention`: another operation (a stop, snapshot or suspend) or a secrets delivery is in progress, another change to this list is still being saved, the computer is being moved to another workspace, its workspace changed while this change was being saved, its disk is still being copied (a create or clone still building), or its host could not read whether it is running just now; send it again once that passes. `stale_revision`: the list changed since the `version` you sent was read, or another change landed while this one was being saved; read it again and make the change against what it says now. `unsupported`: bindings that cannot be read here, a workspace change whose outcome was lost (check it in the dashboard), recorded bindings that belong to a workspace the computer is no longer in (remove them all with `{\"secrets\":[]}`), a computer whose process its host cannot read until support repairs the host, a running session its secrets were never delivered to that nothing is delivering to now (stop it), a first binding while the computer is running or suspended, or a list that drops the browser proxy’s credentials file; waiting changes none of them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 16 KiB (16384 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso a change the computer’s host could not save: nothing was changed, and it can be sent again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PUT \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/secrets' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"secrets\":[{\"secret_id\":\"csec-0123456789abcdef\",\"env\":\"OPENAI_API_KEY\"}],\"version\":1}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"secrets\\\":[{\\\"secret_id\\\":\\\"csec-0123456789abcdef\\\",\\\"env\\\":\\\"OPENAI_API_KEY\\\"}],\\\"version\\\":1}\")\n    c = client.computers.get(id)\n    result = c.set_secrets(body[\"secrets\"], version=body[\"version\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"secrets\":[{\"secret_id\":\"csec-0123456789abcdef\",\"env\":\"OPENAI_API_KEY\"}],\"version\":1};\nconst c = await client.computers.get(id);\nconst result = await c.setSecrets(body.secrets.map((s: {secret_id: string; env?: string; file?: string; revision_id?: string}) => ({secretId: s.secret_id, env: s.env, file: s.file, revisionId: s.revision_id})), {version: body.version});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdSecrets","tags":["Computers"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/clone":{"post":{"summary":"Clone a computer","description":"Builds a new computer from a copy of this one’s current disk. The source is untouched, and must be stopped or suspended: a running source answers 409.\n\nThe answer arrives as soon as the new computer exists, with `status: \"building\"` while its disk is copied — poll it, or wait on its `operation_id`. The clone keeps the source’s size (`size` is a 400), lands stopped, and has no secrets bound.\n\nRequires the `member` role or higher.","operationId":"cloneComputer","tags":["Computers"],"x-rate-cost":100,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Name for the copy. Defaults to the source’s name followed by `-clone`. Cut to 64 characters, with control characters turned into spaces and the ends trimmed. A name left empty by that gets the default too."}}}}}},"responses":{"201":{"description":"The new computer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputerConnect"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"vm-b2c3d4e5f6a7","name":"scratch-copy","status":"building","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"resolution":"1280x800x24","created_at":"2026-09-16T12:00:00.000Z","vnc":{"url":"wss://app.mandala.computer/api/v1/computers/vm-b2c3d4e5f6a7/vnc?token=illustrative-control-token","view_url":"wss://app.mandala.computer/api/v1/computers/vm-b2c3d4e5f6a7/vnc?token=illustrative-view-token","token":"illustrative-control-token","view_token":"illustrative-view-token","terminal_url":"wss://app.mandala.computer/api/v1/computers/vm-b2c3d4e5f6a7/terminal?token=illustrative-control-token","events_url":"wss://app.mandala.computer/api/v1/computers/vm-b2c3d4e5f6a7/events?token=illustrative-control-token","embed_url":"https://app.mandala.computer/embed/desktop#computer=vm-b2c3d4e5f6a7&token=illustrative-view-token","clipboard":true},"operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso, with no `reason`, `Verify your email before launching a computer.`: the person this credential belongs to has not verified their email address. Nothing is created; verify it in the dashboard first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/clone' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"scratch-copy\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"name\\\":\\\"scratch-copy\\\"}\")\n    c = client.computers.get(id)\n    result = c.clone(body[\"name\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"name\":\"scratch-copy\"};\nconst c = await client.computers.get(id);\nconst result = await c.clone(body.name);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdClone","tags":["Computers"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/move":{"post":{"summary":"Move a computer so a resize fits","description":"Consent to moving this computer to another host in its region, so that a resize its current host cannot run becomes possible.\n\nTHIS IS THE SECOND HALF OF A REFUSED RESIZE, and it is only ever the second half. `PATCH /computers/{id}` answers 409 with a `move` object when the size you asked for is more RAM than the host this computer is on can run: `{\"required\":true,\"possible\":true}` means somewhere else in the region can run it, and this endpoint is how you say yes. `possible:false` means nothing in the region can, and there is nothing to call — change the size.\n\nONLY `cpu`, `ram_mb` and `disk_gb` ARE READ. Send the sizing group you sent the PATCH; anything else in the body is ignored rather than refused, so a rename is not carried out here — send that to the PATCH on its own once the move has finished.\n\nIt is a separate call on purpose. A resize that silently relocated your computer is exactly what this is not, so there is no flag on the PATCH that does this for you.\n\nIT ANSWERS BEFORE IT FINISHES. 202, with the move as it stands at that moment; the disk copy runs behind it and takes as long as the disk takes. Poll `GET /moves` until `live` is false. Everything is decided again at the moment this runs — your plan, whether the computer is stopped, and which host it goes to — so a 409 here is real even though the PATCH offered the move.\n\nThe computer must be STOPPED. Suspended is not stopped for this one, unlike a resize: a saved session only loads on the host that wrote it, so it cannot travel. Resume and stop it, or discard the session, first.\n\nONE AT A TIME PER ACCOUNT. A second move while one is running is a 409 whichever computer it names. Naming ANOTHER computer, it carries `reason: \"contention\"` and clears once the running move settles: `GET /moves` says which one it is. Naming the computer already being moved, it carries no `reason`: that move is the answer, so follow it in `GET /moves` rather than sending this again.\n\nTHE COMPUTER’S SNAPSHOTS FOLLOW IT AFTERWARDS. They are carried to the new host after the move answers, and until they land, restoring one taken before the move answers 409 (see `POST /snapshots/{id}/restore`). If the carry fails, those snapshots stay on the old host and can still be cloned, but not restored onto this computer. A clone of one works while the carry runs, except while the new host is taking those snapshots over, when it can also answer `409`; retry it once the carry has finished.\n\nThe target is ours to choose and is never in the request. You are told a host in this region, not which one.\n\nRequires the `member` role or higher.","operationId":"moveComputer","tags":["Moves"],"x-rate-cost":100,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cpu":{"type":"integer","description":"Cores. The same field the resize takes; omit it to leave the count alone."},"ram_mb":{"type":"integer","description":"The size that did not fit. Required: it must be MORE than the computer has now — this endpoint exists for growth that needs a bigger host, and a request that would fit where it is, one leaving this out included, is a 409 rather than a move nobody needed. A body with none of the three, or no body at all, is a 400."},"disk_gb":{"type":"integer","description":"Grow only, as everywhere else. The growth happens on the far side, after the copy."}},"required":["ram_mb"]}}}},"responses":{"202":{"description":"The move, as it stands the moment it was accepted. Read `GET /moves` for the rest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Move"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"computer_id":"vm-a1b2c3d4e5f6","state":"staging","detail":"","live":true,"ram_mb":32768,"started_at":"2026-09-16T12:00:00.000Z","operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.\n\nAlso `reason: \"contention\"` when another computer on this account is being moved: it clears once that move settles, and `GET /moves` says which. A move of this same computer already running is a `409` with no `reason`: follow it in `GET /moves` rather than sending this again. A computer whose disk is still being made carries `contention` too: it lands stopped by itself, and the same request then moves it. A running computer is `reason: \"running\"`: stop it, and send the same request. The move’s other refusals carry no `reason`, and none of them clears by waiting: see above for what each wants changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/move' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"ram_mb\":32768}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"ram_mb\\\":32768}\")\n    c = client.computers.get(id)\n    result = c.relocate(ram_mb=body[\"ram_mb\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"ram_mb\":32768};\nconst c = await client.computers.get(id);\nconst result = await c.relocate({ramMb: body.ram_mb});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdMove","tags":["Moves"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/moves":{"get":{"summary":"List moves","description":"Every move on this account that is worth reading: the ones still running, and the ones that have finished and not yet been dismissed. This is the polling half of `POST /computers/{id}/move`.\n\nA collection rather than a read of one computer, and there are two things to get from that. A move you started is found by its `computer_id`. And a move you did NOT start is what the \"another computer on this account is being moved right now\" refusal is about — only one runs per account at a time, and this is where you find out which one and how far along it is.\n\nFinished moves stay here for a day, so an outcome is still readable by somebody who went away while it ran. Poll on `live`, not on the row disappearing.\n\nAn API key issued against a workspace sees the moves of computers in that workspace only.\n\nRequires the `viewer` role or higher.","operationId":"listMoves","tags":["Moves"],"x-rate-cost":1,"responses":{"200":{"description":"The moves, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveList"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"moves":[{"computer_id":"vm-a1b2c3d4e5f6","state":"staging","detail":"","live":true,"ram_mb":32768,"started_at":"2026-09-16T12:00:00.000Z"}]}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/moves' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.moves.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.moves.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List moves (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListMoves","tags":["Moves"],"responses":{"200":{"description":"The moves, newest first.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsMoves","tags":["Moves"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/operations":{"get":{"summary":"List operations","description":"The operations this account’s lifecycle calls started, newest first. Every accepted create, clone, start, stop, suspend, restart, snapshot restore, resize, move and delete records one and answers its id as `operation_id`. Absent, with the call still done, in the rare case the operation could not be recorded; a client must not treat its absence as a failure of the call. A call that was refused records nothing, since its error is its outcome. Operations are kept for 7 days from when they were recorded.\n\nA call sent with an `Idempotency-Key` is recorded `pending` BEFORE it is carried out, so `idempotency_key=`, asked with a credential of the same scope, finds it even when its answer never reached you: it ends as its answer says, is removed if the call was refused, and ends `failed` with `error.code: \"lost\"` if the platform never learned how it went. A start, stop, suspend or restart sent without one whose host did not answer in time is recorded too, already `failed` with `error.code: \"lost\"`, and its `503` names it; read the computer to see whether it took effect.\n\nMost finish before their call answers and are recorded `succeeded`. A clone is `running` until its disk is copied, and a move until it lands. A move is brought up to date as you read it, and so is a clone, by asking its host — but only when it is among the first 5 unfinished operations on the page other than moves, newest first. A clone further down is answered as last observed; `GET /operations/{id}` brings it up to date.\n\nAn API key issued against a workspace sees the operations of computers in that workspace only — whoever can read a computer can read its operations, and nobody else.\n\nThe same calls made from the dashboard are recorded and listed here too, on the same terms, except a move.\n\nRequires the `viewer` role or higher.","operationId":"listOperations","tags":["Operations"],"x-rate-cost":5,"parameters":[{"name":"computer_id","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,100}$"},"description":"Only this computer’s operations. For a clone, that is the new computer. A value no computer id could be is a `400`."},{"name":"idempotency_key","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Only the operation the call sent with this `Idempotency-Key` recorded, within the key’s 24 hours. Keys are kept per scope, so this finds the operation that a credential of your own scope reserved with the key. An operation recorded under the same key by a credential of another scope (another workspace’s, or a workspace’s when you ask with an account-wide key) is not found by it, even when the unfiltered listing shows it. A value no key could be is a `400`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"How many to answer."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"The `next_cursor` of the page before. A cursor this credential cannot read — another scope’s, one whose operation has expired, or one never given out — is a `400`. An unknown parameter, or one given twice, is a `400`."}],"responses":{"200":{"description":"A page of operations, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperationList"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"operations":[{"id":"op_89abcdef0123456789abcdef","kind":"clone","computer_id":"vm-b2c3d4e5f6a7","state":"running","error":null,"idempotency_key":null,"created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:00.000Z","finished_at":null},{"id":"op_0123456789abcdef01234567","kind":"start","computer_id":"vm-a1b2c3d4e5f6","state":"succeeded","error":null,"idempotency_key":"5b0f1c2e-8a4d-4f7e-9c3b-2d6a1e0f9b8c","created_at":"2026-09-16T11:58:00.000Z","updated_at":"2026-09-16T11:58:00.000Z","finished_at":"2026-09-16T11:58:00.000Z"}],"next_cursor":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/operations' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.operations.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.operations.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List operations (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListOperations","tags":["Operations"],"parameters":[{"name":"computer_id","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,100}$"},"description":"Only this computer’s operations. For a clone, that is the new computer. A value no computer id could be is a `400`."},{"name":"idempotency_key","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Only the operation the call sent with this `Idempotency-Key` recorded, within the key’s 24 hours. Keys are kept per scope, so this finds the operation that a credential of your own scope reserved with the key. An operation recorded under the same key by a credential of another scope (another workspace’s, or a workspace’s when you ask with an account-wide key) is not found by it, even when the unfiltered listing shows it. A value no key could be is a `400`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"How many to answer."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"The `next_cursor` of the page before. A cursor this credential cannot read — another scope’s, one whose operation has expired, or one never given out — is a `400`. An unknown parameter, or one given twice, is a `400`."}],"responses":{"200":{"description":"A page of operations, newest first.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsOperations","tags":["Operations"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/operations/{id}":{"get":{"summary":"Get an operation","description":"One operation, brought up to date: poll this until `state` is `succeeded` or `failed`. An id this credential cannot see is a `404`, the same as one that never existed or has expired — another account’s, and for an API key confined to a workspace, one about a computer outside it. It takes no query parameters, and any is a `400`.\n\nA clone’s host is asked about its computer when this is read. If the host does not answer in time, the operation is answered as last observed rather than as an error.\n\nRequires the `viewer` role or higher.","operationId":"getOperation","tags":["Operations"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The operation id."}],"responses":{"200":{"description":"The operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Operation"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"op_89abcdef0123456789abcdef","kind":"clone","computer_id":"vm-b2c3d4e5f6a7","state":"running","error":null,"idempotency_key":null,"created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:00.000Z","finished_at":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/operations/op_89abcdef0123456789abcdef' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"op_89abcdef0123456789abcdef\"\n    result = client.operations.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"op_89abcdef0123456789abcdef\";\nconst result = await client.operations.get(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get an operation (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetOperation","tags":["Operations"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The operation id."}],"responses":{"200":{"description":"The operation.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsOperationsById","tags":["Operations"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The operation id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/screenshot":{"get":{"summary":"Take a screenshot","description":"A PNG of the desktop, in the coordinate space `resolution` reports. This is the image to hand a model that is about to decide where to click.\n\nIF YOU ARE DRIVING THE DESKTOP, PASS `fresh=1`. A bare call may serve a frame up to 1.5 seconds old — up to 30 seconds while the computer is busy with another operation — which is fast and fine for a thumbnail and wrong for a loop: a model reading a stale frame concludes its click missed and clicks again, which is how a dialog gets dismissed twice.\n\nA suspended computer returns its saved desktop as a JPEG up to 640 pixels wide, with `X-GC-Frame: suspended`, including without `w`. A `w` (or `format=jpeg`) is accepted but ignored there: the saved frame comes back as it was stored, at its own width. This is a stored picture, not a live screen to drive. Suspend keeps that image locally on disk until the session is resumed or discarded; it is not uploaded or included in backups. Older sessions may have no saved frame; those answer 409 with `reason: unavailable`, and starting the computer brings back a live one.\n\nAn explicit `fresh=1` (or `fresh=true`) request against a suspended computer answers 409 with `reason: unavailable`; start it before requesting a live frame. A concurrent lifecycle operation, or another operation holding the screen with no recent frame to serve, answers 409 with `reason: contention`; retry after that operation finishes. A stopped computer answers 409 with `reason: unavailable`, as exec and input do; start it before driving its desktop.\n\n`region`, `scale`, `format` and `quality` shape the picture: a crop, a smaller image, and a cheaper encoding, applied in that order to the same capture a bare call would return. A cropped or scaled image is in its OWN pixel space: to click on something in it, divide its position by the scale and add the region’s `x` and `y`. A suspended computer answers a crop, a scale, `format=png` or `quality` with 409 and `reason: unavailable`, because its saved picture is not the screen.\n\nEvery live answer names the capture it was cut from in `X-GC-Capture`, and gives that capture’s own size in `X-GC-Capture-Size` (for example `3840x2160`) — the pixels `region` is read in, which is usually `resolution` but not always: a desktop resumed from a capture taken at another size, or resized from inside the guest, can differ. TO CUT A CROP FROM THE CAPTURE YOU MEASURED, send its name back as `capture`: the answer comes from that capture and no other, and takes no new one. Only the newest capture of a computer is held, so the next one replaces it — an ordinary call 1.5 seconds later at the earliest, a `fresh=1` call at once. It is also lost when the daemon on its host restarts or the computer moves to another host. A name that is no longer held answers 409 with `reason: stale_capture` rather than a crop of a different picture. Take a new screenshot and use the name it carries. Measuring on a small `w` thumbnail and then cropping by `capture` costs two small requests, where measuring on the whole PNG costs the screen’s full size. A suspended computer has no capture: its saved picture carries neither header, and `capture` on it answers 409 with `reason: unavailable`.\n\nA query parameter not listed here, or one given twice, answers 400 naming the parameters this route takes, as does a value it cannot read.\n\nRequires the `viewer` role or higher.","operationId":"takeScreenshot","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"fresh","in":"query","required":false,"schema":{"type":"string","enum":["1","true","0","false"]},"description":"Skip the cache and capture now: `1` or `true`. `0` or `false` is the same as leaving it out. Pass this whenever the screenshot is feeding a decision."},{"name":"capture","in":"query","required":false,"schema":{"type":"string","pattern":"^[0-9a-f]{16}$"},"description":"Answer from the capture an earlier screenshot named in `X-GC-Capture`, rather than from the screen now, so a crop can be worked out on one answer and cut from the same pixels by the next. A capture that is no longer held answers 409 with `reason: stale_capture`. Not with `fresh`, which asks for a capture taken after the request."},{"name":"w","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"Downscale to this width and answer JPEG instead of PNG unless `format` says otherwise. For thumbnails. A positive whole number: `0`, a negative number or anything else is a 400. A width below 64 is raised to 64, and one wider than what is being shrunk — the screen, or the `region` — is lowered to its width, so the picture is never enlarged. Not with `scale`. On a suspended computer it is accepted and ignored: the saved frame (up to 640 pixels wide) comes back unchanged."},{"name":"scale","in":"query","required":false,"schema":{"type":"number","exclusiveMinimum":0,"maximum":1},"description":"Downscale by this factor, greater than 0 and at most 1, keeping the encoding PNG unless `format` says otherwise. `0.5` halves both sides, which is a quarter of the pixels a model is sent. Not with `w`."},{"name":"region","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d+,\\d+,\\d+,\\d+$"},"description":"Crop to `x,y,width,height`, in the capture’s own pixels — the size `X-GC-Capture-Size` reports, which is ordinarily `resolution` — before any scaling. The region has to lie inside the capture: one that reaches past an edge answers 400 naming its size, rather than being clipped to a smaller picture than you asked for. With `capture`, it is cut from that capture."},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["png","jpeg","jpg"]},"description":"The encoding. PNG by default, JPEG by default with `w`."},{"name":"quality","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"JPEG quality, 1 to 100; 72 when not given. With a PNG answer it is a 400."}],"responses":{"200":{"description":"The current screen, or the region of it asked for, as PNG — or JPEG with `w` or `format=jpeg`. A live answer carries X-GC-Capture (the capture it was cut from, to send back as `capture`) and X-GC-Capture-Size (that capture’s width x height in pixels). Suspended sessions return a saved JPEG marked X-GC-Frame: suspended, with neither.","content":{"image/png":{"schema":{"type":"string","format":"binary"},"examples":{"success":{"summary":"Illustrative synthetic PNG; the response contains image bytes.","externalValue":"https://app.mandala.computer/docs/examples/screenshot.png"}}},"image/jpeg":{"schema":{"type":"string","format":"binary"},"examples":{"jpeg-thumbnail":{"summary":"Illustrative binary JPEG response for w=640; body is raw JPEG bytes.","externalValue":"https://app.mandala.computer/docs/examples/screenshot.jpg"}}}},"headers":{"X-GC-Capture":{"description":"On a live answer only: the capture this picture was cut from. Send it back as `capture` to cut another crop from the same pixels. Absent on a saved suspended frame, and on a frame the host could not hold.","schema":{"type":"string","pattern":"^[0-9a-f]{16}$"}},"X-GC-Capture-Size":{"description":"On a live answer only: that capture’s own width and height in pixels, the space `region` is read in.","schema":{"type":"string","pattern":"^[0-9]+x[0-9]+$"}},"X-GC-Frame":{"description":"Present only on a suspended computer’s saved desktop, a stored JPEG rather than the screen.","schema":{"type":"string","enum":["suspended"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/screenshot' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -o screen.png"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.screenshot()\n    Path(\"screen.png\").write_bytes(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { writeFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.screenshot();\nawait writeFile(\"screen.png\", result);"}]},"head":{"summary":"Take a screenshot (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headTakeScreenshot","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"fresh","in":"query","required":false,"schema":{"type":"string","enum":["1","true","0","false"]},"description":"Skip the cache and capture now: `1` or `true`. `0` or `false` is the same as leaving it out. Pass this whenever the screenshot is feeding a decision."},{"name":"capture","in":"query","required":false,"schema":{"type":"string","pattern":"^[0-9a-f]{16}$"},"description":"Answer from the capture an earlier screenshot named in `X-GC-Capture`, rather than from the screen now, so a crop can be worked out on one answer and cut from the same pixels by the next. A capture that is no longer held answers 409 with `reason: stale_capture`. Not with `fresh`, which asks for a capture taken after the request."},{"name":"w","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"Downscale to this width and answer JPEG instead of PNG unless `format` says otherwise. For thumbnails. A positive whole number: `0`, a negative number or anything else is a 400. A width below 64 is raised to 64, and one wider than what is being shrunk — the screen, or the `region` — is lowered to its width, so the picture is never enlarged. Not with `scale`. On a suspended computer it is accepted and ignored: the saved frame (up to 640 pixels wide) comes back unchanged."},{"name":"scale","in":"query","required":false,"schema":{"type":"number","exclusiveMinimum":0,"maximum":1},"description":"Downscale by this factor, greater than 0 and at most 1, keeping the encoding PNG unless `format` says otherwise. `0.5` halves both sides, which is a quarter of the pixels a model is sent. Not with `w`."},{"name":"region","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d+,\\d+,\\d+,\\d+$"},"description":"Crop to `x,y,width,height`, in the capture’s own pixels — the size `X-GC-Capture-Size` reports, which is ordinarily `resolution` — before any scaling. The region has to lie inside the capture: one that reaches past an edge answers 400 naming its size, rather than being clipped to a smaller picture than you asked for. With `capture`, it is cut from that capture."},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["png","jpeg","jpg"]},"description":"The encoding. PNG by default, JPEG by default with `w`."},{"name":"quality","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"JPEG quality, 1 to 100; 72 when not given. With a PNG answer it is a 400."}],"responses":{"200":{"description":"The current screen, or the region of it asked for, as PNG — or JPEG with `w` or `format=jpeg`. A live answer carries X-GC-Capture (the capture it was cut from, to send back as `capture`) and X-GC-Capture-Size (that capture’s width x height in pixels). Suspended sessions return a saved JPEG marked X-GC-Frame: suspended, with neither.","headers":{"X-GC-Capture":{"description":"On a live answer only: the capture this picture was cut from. Send it back as `capture` to cut another crop from the same pixels. Absent on a saved suspended frame, and on a frame the host could not hold.","schema":{"type":"string","pattern":"^[0-9a-f]{16}$"}},"X-GC-Capture-Size":{"description":"On a live answer only: that capture’s own width and height in pixels, the space `region` is read in.","schema":{"type":"string","pattern":"^[0-9]+x[0-9]+$"}},"X-GC-Frame":{"description":"Present only on a suspended computer’s saved desktop, a stored JPEG rather than the screen.","schema":{"type":"string","enum":["suspended"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdScreenshot","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/executions/{executionId}/retained-output":{"post":{"summary":"Capture a retained output prefix","description":"Explicitly freeze observed background output into a new immutable result. Retrying creates another result; it never executes the command. No automatic capture, Activities record, listing or idempotency key. Accepts an application/json object (including {}) of at most 4096 bytes. The total capture deadline is 60000 milliseconds. Capture starts at independent zero offsets, stops each stream at observed EOF or its requested cap, and stores the wrapper diagnostic once. This is a finite prefix from mutable guest paths, not an atomic snapshot or completion proof. Expiry is measured from capture_started_at. No query parameters or arbitrary metadata are accepted. A lost response may leave a committed result until expiry. Responses are no-store; Location is the relative metadata URL.\n\nRequires the `member` role or higher.","operationId":"captureRetainedOutput","tags":["Retained results"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"executionId","in":"path","required":true,"schema":{"type":"string"},"description":"The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"max_bytes_per_stream":{"type":"integer","minimum":1,"maximum":4194304,"default":1048576},"retention_seconds":{"type":"integer","minimum":1,"maximum":604800,"default":86400}},"additionalProperties":false}}}},"responses":{"201":{"description":"Published immutable metadata, never raw output. The original execution remains a separate volatile handle.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetainedResult"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"version":1,"result_id":"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6","kind":"background-output","state":"ready","account_id":"acc-d5e6f7a8b9c0d1e2","computer_id":"vm-a1b2c3d4e5f6","workspace_id":null,"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","capture_started_at":"2026-09-16T12:00:00.000Z","captured_at":"2026-09-16T12:00:01.000Z","expires_at":"2026-09-17T12:00:00.000Z","source":"volatile_guest_files","execution_observation":{"status":"exited","observed_at":"2026-09-16T12:00:01.000Z","exit_code":0},"stdout":{"bytes":20,"sha256":"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde","source_offset":0,"next_source_offset":20,"end_reason":"observed_eof"},"stderr":{"bytes":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","source_offset":0,"next_source_offset":0,"end_reason":"observed_eof"},"diagnostic":{"bytes":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","source":"wrapper","diagnostic_truncated":false}}}}}},"headers":{"Location":{"description":"The new result’s metadata URL, as a path on this host: `GET` it to read the result.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.\n\nThe execution or recorded/current computer scope is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nThe guest output cannot be read; no result is published (`output_unavailable`). It carries the host’s `reason` when it gave one: `contention` (its guest agent busy with another call) clears by itself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nRate limit or store quota/concurrency capacity; store capacity includes Retry-After:60.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.\n\nThe source transfer or finite foundation protocol is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nFresh scope, exclusive store ownership or storage is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.\n\nCapture deadline or cancellation; incomplete transfer is not published.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/executions/exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4/retained-output' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    executionId = \"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\"\n    c = client.computers.get(id)\n    result = c.retain_execution_output(executionId)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst executionId = \"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\";\nconst c = await client.computers.get(id);\nconst result = await c.retainExecutionOutput(executionId);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdExecutionsByExecutionIdRetainedOutput","tags":["Retained results"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"executionId","in":"path","required":true,"schema":{"type":"string"},"description":"The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/results/{resultId}":{"get":{"summary":"Read retained result metadata","description":"Member-only immutable metadata with recorded scope and fresh daemon computer metadata checks. No guest output, files, execution handle, wake or Activity capture. Retained reads have a 15000 millisecond total deadline. Current host unavailability prevents access even when bytes exist. Missing, expired, deleted and outside-scope results share 404. No query parameters. Reads do not extend expiry and all responses are no-store.\n\nRequires the `member` role or higher.","operationId":"getRetainedResult","tags":["Retained results"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":"A `result_id` from a retained `exec` (`res_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Finite recorded metadata. Digest identifies the bytes hashed at capture; bounded reads validate file type and size without rehashing the entire object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutputRetainedResult"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"version":1,"result_id":"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6","kind":"background-output","state":"ready","account_id":"acc-d5e6f7a8b9c0d1e2","computer_id":"vm-a1b2c3d4e5f6","workspace_id":null,"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","capture_started_at":"2026-09-16T12:00:00.000Z","captured_at":"2026-09-16T12:00:01.000Z","expires_at":"2026-09-17T12:00:00.000Z","source":"volatile_guest_files","execution_observation":{"status":"exited","observed_at":"2026-09-16T12:00:01.000Z","exit_code":0},"stdout":{"bytes":20,"sha256":"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde","source_offset":0,"next_source_offset":20,"end_reason":"observed_eof"},"stderr":{"bytes":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","source_offset":0,"next_source_offset":0,"end_reason":"observed_eof"},"diagnostic":{"bytes":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","source":"wrapper","diagnostic_truncated":false}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified; never an empty ready result. A read that ran out of time can be sent again. But when a stored file or its record fails its check, or the file cannot be read, the result is withdrawn as this answer is given, if storage can record that: later reads and deletes answer `404`. So retry this result first, and capture a new one only if the retry answers `404`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/results/res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    resultId = \"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.result(resultId)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst resultId = \"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.result(resultId);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read retained result metadata (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetRetainedResult","tags":["Retained results"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":"A `result_id` from a retained `exec` (`res_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Finite recorded metadata. Digest identifies the bytes hashed at capture; bounded reads validate file type and size without rehashing the entire object.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified; never an empty ready result. A read that ran out of time can be sent again. But when a stored file or its record fails its check, or the file cannot be read, the result is withdrawn as this answer is given, if storage can record that: later reads and deletes answer `404`. So retry this result first, and capture a new one only if the retry answers `404`.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"delete":{"summary":"Delete a retained result","description":"Member-only logical deletion after recorded and fresh current scope checks. No body or query. Later reads and repeated deletes return 404. Physical cleanup is bounded and remains charged until confirmed removal. No guest work; host outage returns unavailable rather than guessing access. Responses are no-store.\n\nRequires the `member` role or higher.","operationId":"deleteRetainedResult","tags":["Retained results"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":"A `result_id` from a retained `exec` (`res_` followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"No response body; the result is logically unavailable.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified, and the result may be left in place: send the delete again until it answers `204` or `404`. When its stored record failed its check, the result is withdrawn as this answer is given if storage can record that, and the resend answers `404`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/results/res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    resultId = \"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.delete_result(resultId)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst resultId = \"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.deleteResult(resultId);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdResultsByResultId","tags":["Retained results"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":"A `result_id` from a retained `exec` (`res_` followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/artifacts":{"post":{"summary":"Publish a nominated immutable artifact","description":"Explicitly capture one nominated guest file into immutable private retained storage. Accepts at most 32768 bytes of application/json and a 60000 millisecond control-plane deadline. The computer must already be running: one source file GET uses no_wake=1 and may touch guest activity. No automatic capture, shell, directory scan, Range assembly or retry on another host. Exact transferred bytes must match expected_size and expected_sha256; no prefix success. A live path may change or follow symlinks; digest matching is not an atomic snapshot or proof of who created a file. Optional execution_id must currently resolve on the same computer and recorded scope; its caller_selected association does not prove creation. Retrying creates another version; no listing or idempotency key. created_at is final publication time; expiry starts at reservation and never renews on reads. All routes require member and refuse suspended accounts. Artifacts and retained output share one store quota: at most 128 MiB and 256 objects per computer, 512 MiB and 2000 objects per account, and 2 captures in progress per account. An artifact is charged at expected_size from reservation; a retained-output capture at twice max_bytes_per_stream, plus the diagnostic for background output. Deleted objects stay charged until physical cleanup. Past any limit, or when the platform store is full, the answer is 429 result_capacity with Retry-After: 60. No query parameters or caller metadata. A verified execution association may add a passive result link to its existing activity. Responses are no-store with a relative Location. Daemon guest transfer can outlive control-plane cancellation.\n\nRequires the `member` role or higher.","operationId":"publishArtifact","tags":["Retained results"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"type":"string","maxLength":4096,"description":"Exact OS-absolute guest path, at most 4096 UTF-8 bytes, valid Unicode with no controls/DEL. Spaces and Linux backslashes are preserved."},"expected_size":{"type":"integer","minimum":0,"maximum":67108864},"expected_sha256":{"type":"string","pattern":"^[a-f0-9]{64}$"},"execution_id":{"type":"string","pattern":"^exec_[a-f0-9]{32}$"},"max_bytes":{"type":"integer","minimum":1,"maximum":67108864,"default":8388608,"description":"Expected size must not exceed this cap. Larger files require explicit opt-in above the default."},"retention_seconds":{"type":"integer","minimum":1,"maximum":604800,"default":86400}},"required":["path","expected_size","expected_sha256"],"additionalProperties":false}}}},"responses":{"201":{"description":"Finite immutable artifact metadata, without source path, account attribution, command or raw content.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Artifact"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"artifact_id":"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5","kind":"artifact","state":"ready","computer_id":"vm-a1b2c3d4e5f6","workspace_id":null,"created_at":"2026-09-16T12:00:00.000Z","expires_at":"2026-09-17T12:00:00.000Z","size":20,"sha256":"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde","execution_association":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.\n\nThe source computer or the nominated guest file is unavailable (`artifact_unavailable`), or the `execution_id` does not resolve on this computer and scope (`execution_unavailable`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nThe computer is not running or the source cannot be read now (`artifact_source_unavailable`), or the transferred size/digest differs from the nomination (`artifact_mismatch`). `artifact_source_unavailable` carries the host’s `reason` when it gave one: `contention` (its guest agent busy with another call, a suspend or move in flight) clears by itself, and `unavailable` means the computer is not running.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThe body is over 32768 bytes (`capture_request_too_large`), or `expected_size` exceeds `max_bytes`, or the guest file is larger than the 64 MiB one guest file transfer carries (`artifact_too_large`). A file larger than `expected_size` but within that ceiling is a `409` `artifact_mismatch` instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nRate or shared retained quota/concurrency limit; store capacity is `result_capacity`, with Retry-After:60.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.\n\nInvalid or failed source transport (`capture_source_invalid`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope (`scope_unavailable`) or private store (`result_store_unavailable`) unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.\n\nCapture deadline or cancellation (`artifact_timeout`); no incomplete object is published.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/artifacts' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"path\":\"/tmp/report.bin\",\"expected_size\":20,\"expected_sha256\":\"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"path\\\":\\\"/tmp/report.bin\\\",\\\"expected_size\\\":20,\\\"expected_sha256\\\":\\\"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde\\\"}\")\n    c = client.computers.get(id)\n    result = c.publish_artifact(body[\"path\"], expected_size=body[\"expected_size\"], expected_sha256=body[\"expected_sha256\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"path\":\"/tmp/report.bin\",\"expected_size\":20,\"expected_sha256\":\"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde\"};\nconst c = await client.computers.get(id);\nconst result = await c.publishArtifact(body.path, {expectedSize: body.expected_size, expectedSha256: body.expected_sha256});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdArtifacts","tags":["Retained results"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/artifacts/{artifactId}":{"get":{"summary":"Read immutable artifact metadata","description":"Member-only retained metadata with original credential and fresh exact computer scope checks; a 15000 millisecond total deadline. No source path, execution handle, guest agent, wake or Activity capture. Stopped computers remain readable through daemon metadata; current host outage prevents access even when retained bytes exist. Unknown, foreign, expired and deleted artifacts share 404. Corrupt/missing retained files are unavailable, never empty success or a live fallback. No query parameters; no-store; expiry never renews.\n\nRequires the `member` role or higher.","operationId":"getArtifact","tags":["Retained results"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"An `artifact_id` from `POST /computers/{id}/artifacts` (`art_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"The immutable manifest. Digest identifies capture bytes; reads verify file type/layout/size without a full checksum scan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Artifact"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"artifact_id":"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5","kind":"artifact","state":"ready","computer_id":"vm-a1b2c3d4e5f6","workspace_id":null,"created_at":"2026-09-16T12:00:00.000Z","expires_at":"2026-09-17T12:00:00.000Z","size":20,"sha256":"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde","execution_association":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified; never an empty success. But when a stored file or its record fails its check, or the file cannot be read, the artifact is withdrawn as this answer is given, if storage can record that: later reads and deletes answer `404`. So retry this artifact first, and capture a new one only if the retry answers `404`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/artifacts/art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    artifactId = \"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\"\n    c = client.computers.get(id)\n    result = c.artifact(artifactId)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst artifactId = \"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\";\nconst c = await client.computers.get(id);\nconst result = await c.artifact(artifactId);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read immutable artifact metadata (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetArtifact","tags":["Retained results"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"An `artifact_id` from `POST /computers/{id}/artifacts` (`art_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"The immutable manifest. Digest identifies capture bytes; reads verify file type/layout/size without a full checksum scan.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified; never an empty success. But when a stored file or its record fails its check, or the file cannot be read, the artifact is withdrawn as this answer is given, if storage can record that: later reads and deletes answer `404`. So retry this artifact first, and capture a new one only if the retry answers `404`.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"delete":{"summary":"Delete an immutable artifact","description":"Member-only durable tombstone after original credential and fresh recorded/current computer scope checks. No guest calls, body or query. Repeated deletes and reads return 404. Physical cleanup remains charged until descriptors and files are removed. Suspended accounts are refused; no-store.\n\nRequires the `member` role or higher.","operationId":"deleteArtifact","tags":["Retained results"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"An `artifact_id` from `POST /computers/{id}/artifacts` (`art_` followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"No response body; later artifact reads are unavailable.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified, and the artifact may be left in place: send the delete again until it answers `204` or `404`. When its stored record failed its check, the artifact is withdrawn as this answer is given if storage can record that, and the resend answers `404`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/artifacts/art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    artifactId = \"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\"\n    c = client.computers.get(id)\n    result = c.delete_artifact(artifactId)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst artifactId = \"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\";\nconst c = await client.computers.get(id);\nconst result = await c.deleteArtifact(artifactId);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdArtifactsByArtifactId","tags":["Retained results"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"An `artifact_id` from `POST /computers/{id}/artifacts` (`art_` followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/artifacts/{artifactId}/download":{"get":{"summary":"Download immutable artifact bytes","description":"Full retained binary download, at most 67108864 bytes with a 60000 millisecond overall budget. No guest or execution calls. Shared read slots/pins remain held through stream and pending local I/O completion. Backpressure queues at most one 64-KiB payload chunk. Original authority and current row/generation/expiry are checked before enqueue; fresh exact computer scope is required after one second or one MiB, whichever first. Revocation cannot retract already delivered or queued bytes. Errors after headers terminate the stream; a chunk that fails its check or cannot be read also withdraws the artifact, if storage can record that: retry it, and capture a new one only if the retry answers 404. Content-Length is exact, Content-Disposition is a generated art_<hex>.bin attachment, with nosniff, no-store and Accept-Ranges:none. Incoming Range is ignored with a full 200 response. No query parameters.\n\nRequires the `member` role or higher.","operationId":"downloadArtifact","tags":["Retained results"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"An `artifact_id` from `POST /computers/{id}/artifacts` (`art_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Raw immutable bytes, never base64 or an inline preview.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"},"examples":{"success":{"summary":"Raw retained artifact bytes, including final LF.","value":"Hello from Mandala.\n"}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified; never an empty success. But when a stored file or its record fails its check, or the file cannot be read, the artifact is withdrawn as this answer is given, if storage can record that: later reads and deletes answer `404`. So retry this artifact first, and capture a new one only if the retry answers `404`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/artifacts/art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5/download' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -o download.bin"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    artifactId = \"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\"\n    c = client.computers.get(id)\n    result = c.download_artifact(artifactId, max_bytes=64 * 1024 * 1024)\n    Path(\"download.bin\").write_bytes(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { writeFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst artifactId = \"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\";\nconst c = await client.computers.get(id);\nconst result = await c.downloadArtifact(artifactId, {maxBytes: 64 * 1024 * 1024});\nawait writeFile(\"download.bin\", result);"}]},"head":{"summary":"Download immutable artifact bytes (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. This one checks metadata instead of reading the content: it never resumes a guest, consumes output or opens a stream, so a header or failure that only reading the content could produce may be missing.\n\nRequires the `member` role or higher.","operationId":"headDownloadArtifact","tags":["Retained results"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"An `artifact_id` from `POST /computers/{id}/artifacts` (`art_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Raw immutable bytes, never base64 or an inline preview.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified; never an empty success. But when a stored file or its record fails its check, or the file cannot be read, the artifact is withdrawn as this answer is given, if storage can record that: later reads and deletes answer `404`. So retry this artifact first, and capture a new one only if the retry answers `404`.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdArtifactsByArtifactIdDownload","tags":["Retained results"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"An `artifact_id` from `POST /computers/{id}/artifacts` (`art_` followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/results/{resultId}/output":{"get":{"summary":"Read an independent retained byte range","description":"One bounded raw byte range from immutable control-plane storage, with fresh authorization before and after the read. No guest or execution-handle calls. Independent retries never consume a cursor or change expiry. At or beyond retained EOF the body is empty, next offset is unchanged and EOF is true. Unknown or duplicate query parameters and unsafe offset+limit are refused. Range headers are not supported. Response headers: X-Result-Offset (requested byte offset), X-Result-Next-Offset (offset plus returned bytes), X-Result-EOF (true or false for the retained stream), exact Content-Length, attachment Content-Disposition with a generated filename, nosniff and no-store.\n\nRequires the `member` role or higher.","operationId":"readRetainedOutput","tags":["Retained results"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":"A `result_id` from a retained `exec` (`res_` followed by 32 lowercase hex characters)."},{"name":"stream","in":"query","required":true,"schema":{"type":"string","enum":["stdout","stderr","diagnostic"]},"description":"The retained stream to read. Synchronous output has no diagnostic; requesting it returns result_stream_unavailable 409."},{"name":"offset","in":"query","required":true,"schema":{"type":"integer","minimum":0,"maximum":9007199254740990},"description":"Explicit nonnegative decimal byte offset; offset plus limit must be a safe integer."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":65536,"default":65536},"description":"Maximum response bytes."}],"responses":{"200":{"description":"Raw bytes, never base64. EOF describes the retained prefix only.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"},"examples":{"success":{"summary":"Raw retained stdout prefix bytes for stream=stdout&offset=0.","value":"Hello from Mandala.\n"}}}},"headers":{"X-Result-Offset":{"description":"The byte offset that was asked for.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Result-Next-Offset":{"description":"The offset plus the bytes returned: the `offset` to ask for next.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Result-EOF":{"description":"`true` once this range reaches the end of the retained stream, otherwise `false`.","schema":{"type":"string","enum":["true","false"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified; never an empty ready result. A read that ran out of time can be sent again. But when a stored file or its record fails its check, or the file cannot be read, the result is withdrawn as this answer is given, if storage can record that: later reads and deletes answer `404`. So retry this result first, and capture a new one only if the retry answers `404`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/results/res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6/output?stream=stdout&offset=0' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -o download.bin"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    resultId = \"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\"\n    query = {\"stream\":\"stdout\",\"offset\":\"0\"}\n    c = client.computers.get(id)\n    result = c.result_output(resultId, stream=query[\"stream\"], offset=int(query[\"offset\"]))\n    Path(\"download.bin\").write_bytes(result.data)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { writeFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst resultId = \"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\";\nconst query = {\"stream\":\"stdout\",\"offset\":\"0\"};\nconst c = await client.computers.get(id);\nconst result = await c.resultOutput(resultId, {stream: \"stdout\", offset: Number(query.offset)});\nawait writeFile(\"download.bin\", result.bytes);"}]},"head":{"summary":"Read an independent retained byte range (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. This one checks metadata instead of reading the content: it never resumes a guest, consumes output or opens a stream, so a header or failure that only reading the content could produce may be missing.\n\nRequires the `member` role or higher.","operationId":"headReadRetainedOutput","tags":["Retained results"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":"A `result_id` from a retained `exec` (`res_` followed by 32 lowercase hex characters)."},{"name":"stream","in":"query","required":true,"schema":{"type":"string","enum":["stdout","stderr","diagnostic"]},"description":"The retained stream to read. Synchronous output has no diagnostic; requesting it returns result_stream_unavailable 409."},{"name":"offset","in":"query","required":true,"schema":{"type":"integer","minimum":0,"maximum":9007199254740990},"description":"Explicit nonnegative decimal byte offset; offset plus limit must be a safe integer."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":65536,"default":65536},"description":"Maximum response bytes."}],"responses":{"200":{"description":"Raw bytes, never base64. EOF describes the retained prefix only.","headers":{"X-Result-Offset":{"description":"The byte offset that was asked for.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Result-Next-Offset":{"description":"The offset plus the bytes returned: the `offset` to ask for next.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Result-EOF":{"description":"`true` once this range reaches the end of the retained stream, otherwise `false`.","schema":{"type":"string","enum":["true","false"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or local backing storage cannot be verified; never an empty ready result. A read that ran out of time can be sent again. But when a stored file or its record fails its check, or the file cannot be read, the result is withdrawn as this answer is given, if storage can record that: later reads and deletes answer `404`. So retry this result first, and capture a new one only if the retry answers `404`.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdResultsByResultIdOutput","tags":["Retained results"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":"A `result_id` from a retained `exec` (`res_` followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/signals":{"get":{"summary":"Read passive platform signals","description":"Member-only, finite reads of daemon facts. No guest connection, wake, watcher, output read or Activity capture. An absent or empty since starts at the current head with baseline:true and no history. Replay uses the full opaque cursor; it advances past filtered rows. An expired, malformed, restarted or migrated epoch returns an explicit gap with a new head. These are ephemeral observations, not durable history, complete process coverage or task-success proof. Pages contain at most 100 events and 65536 encoded bytes. Query strings are at most 4096 characters; unknown or duplicate parameters are refused. Both emission scope and current daemon scope must match. Requests have finite routing/body deadlines and may fail unavailable; failures never become empty history. Responses are no-store.\n\nRequires the `member` role or higher.","operationId":"readPlatformSignals","tags":["Platform signals"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"since","in":"query","required":false,"schema":{"type":"string","maxLength":2048},"description":"Opaque cursor from the previous page. Missing or empty establishes a head-only baseline."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Maximum eligible events on this page."}],"responses":{"200":{"description":"A safe baseline, finite replay page or explicit reset gap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSignalPage"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"computer":"vm-a1b2c3d4e5f6","from":"dljjqbv5mo00:0","cursor":"dljjqbv5mo00:0","events":[],"more":false,"baseline":true,"supported":["process.exited","computer.started","computer.stopped","computer.suspended","computer.idle"],"retention":"ephemeral"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.\n\nComplete scoped discovery proves the computer missing or outside scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nThe ordinary rate limit or bounded in-flight capacity is exhausted. Capacity refusals include Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"501":{"description":"This feature is not available: no hypervisor in the fleet can build a template, or the platform or the host holding this computer does not offer it. Unlike a `409`, this does not clear on its own — retrying will not help, so treat it as an outage of the feature rather than of the moment.\n\nA scoped host still holds this computer but lacks signals. Body code is signals_unsupported and reason is unsupported. No active observer fallback.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nHost discovery, scope, protocol validation or the bounded transport is unavailable. Body code is signals_unavailable; keep the prior checkpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/signals' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.signals()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.signals();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read passive platform signals (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headReadPlatformSignals","tags":["Platform signals"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"since","in":"query","required":false,"schema":{"type":"string","maxLength":2048},"description":"Opaque cursor from the previous page. Missing or empty establishes a head-only baseline."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Maximum eligible events on this page."}],"responses":{"200":{"description":"A safe baseline, finite replay page or explicit reset gap.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.\n\nComplete scoped discovery proves the computer missing or outside scope.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nThe ordinary rate limit or bounded in-flight capacity is exhausted. Capacity refusals include Retry-After.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"501":{"description":"This feature is not available: no hypervisor in the fleet can build a template, or the platform or the host holding this computer does not offer it. Unlike a `409`, this does not clear on its own — retrying will not help, so treat it as an outage of the feature rather than of the moment.\n\nA scoped host still holds this computer but lacks signals. Body code is signals_unsupported and reason is unsupported. No active observer fallback.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nHost discovery, scope, protocol validation or the bounded transport is unavailable. Body code is signals_unavailable; keep the prior checkpoint.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdSignals","tags":["Platform signals"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/activities":{"get":{"summary":"Read API activity history","description":"Selected requests sent through the computer API. This does not identify an agent or show all guest work. A hosted agent run (`POST /computers/{id}/agent`, `POST /chat/completions` or the dashboard agent) adds one summary row with channel `managed-agent` and action `agent`, carrying `funded` and `cost_usd`. It stays pending while the run lasts, for at most about two hours. Each input or `bash` command the run sends to the desktop is also recorded as its own row with channel `managed-agent`, so one run can add many rows; its screenshots are not recorded. Requests from an API key carry channel `api`, and the dashboard’s own carry `session-api`. History retains up to 7 days, 10000 rows per computer and 100000 per account, including pending rows. Each safe metadata row is at most 1024 bytes; pages contain at most 50 rows. Expired rows are excluded immediately; bounded periodic physical cleanup can lag with backlog or downtime. Pages are newest first with a fixed insertion watermark. Cursor tokens are opaque and bound to account, computer, current workspace and credential scope. Use changes=1 with changes_cursor to receive inserts and final updates, including old rows finishing. Follow next_cursor to drain a bounded change page. Send next_cursor back with the same changes value it came with: a history cursor without changes, a change cursor with changes=1. A history cursor that is invalid, expired, minted for another computer or credential scope, or a change cursor sent without changes=1, answers 400; it never restarts at the first page. In changes mode, any cursor that cannot continue (expired, invalid, or a history cursor) answers gap:true instead. A gap requires refreshing history; reads never replay an action. Member or owner access and fresh computer scope are required for every read. Reads work while stopped or suspended without waking the guest. Unavailable scope or storage answers 503 with incomplete:true, never an empty success. Capture is best effort. Unknown outcomes are never resolved by reading output; an accepted background request is not completion. No command, output, input text or arbitrary error prose is retained. Any other query parameter, or either one sent twice, is a 400.\n\nRequires the `member` role or higher.","operationId":"listActivities","tags":["Activities"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque next_cursor for older pages, or changes_cursor when changes=1. At most 2048 characters."},{"name":"changes","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Read the bounded change journal. Requires a cursor (a missing one is a 400). A cursor that cannot continue, expired, invalid or a history cursor, returns gap:true."}],"responses":{"200":{"description":"Safe retained rows or change projections with retention and capture health. Responses are no-store.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivityPage"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"items":[{"activity_id":"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7","account_id":"acc-d5e6f7a8b9c0d1e2","computer_id":"vm-a1b2c3d4e5f6","workspace_id":null,"channel":"api","route":"exec","action":"background-exec","state":"accepted","received_at":"2026-09-16T12:00:00.000Z","observed_at":"2026-09-16T12:00:01.000Z","revision":2,"has_results":true,"dispatched_at":"2026-09-16T12:00:00.000Z","elapsed_ms":1000,"http_status":202,"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"}],"next_cursor":null,"changes_cursor":"eyJ2IjoxLCJraW5kIjoiY2hhbmdlcyIsInNjb3BlIjp7ImFjY291bnRfaWQiOiJhY2MtZDVlNmY3YThiOWMwZDFlMiIsImNvbXB1dGVyX2lkIjoidm0tYTFiMmMzZDRlNWY2Iiwid29ya3NwYWNlX2lkIjpudWxsfSwicmVzdHJpY3Rpb24iOm51bGwsIndhdGVybWFyayI6MiwiZXhwaXJlcyI6MTc4OTY0NjQwMDAwMH0.83fvzFcDxh2LUQRZNx5mBeCF9-O7diT9fsiroQdK4rU","gap":false,"health":{"recording_started_at":"2026-09-10T00:00:00.000Z","earliest_retained_at":"2026-09-16T12:00:00.000Z","count_truncated":false,"age_truncated":false,"capture":"available","completeness":"best-effort","gap_at":null,"recovered_at":null}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nFresh scope or the activity store is unavailable. The body includes incomplete:true; this is not evidence of no activity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/activities' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.activities()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.activities();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read API activity history (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headListActivities","tags":["Activities"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque next_cursor for older pages, or changes_cursor when changes=1. At most 2048 characters."},{"name":"changes","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Read the bounded change journal. Requires a cursor (a missing one is a 400). A cursor that cannot continue, expired, invalid or a history cursor, returns gap:true."}],"responses":{"200":{"description":"Safe retained rows or change projections with retention and capture health. Responses are no-store.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nFresh scope or the activity store is unavailable. The body includes incomplete:true; this is not evidence of no activity.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdActivities","tags":["Activities"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/activities/{activity}":{"get":{"summary":"Read one API activity","description":"One retained safe summary. Current computer access and the row’s recorded account/workspace scope are checked again. Missing, expired, deleted and outside-scope rows answer the same 404. No output or guest files are read. No query parameters are accepted.\n\nRequires the `member` role or higher.","operationId":"getActivity","tags":["Activities"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"activity","in":"path","required":true,"schema":{"type":"string"},"description":"An `activity_id` from `GET /computers/{id}/activities` (`act_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Safe activity metadata only; never raw execution content. Responses are no-store.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Activity"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"activity_id":"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7","account_id":"acc-d5e6f7a8b9c0d1e2","computer_id":"vm-a1b2c3d4e5f6","workspace_id":null,"channel":"api","route":"exec","action":"background-exec","state":"accepted","received_at":"2026-09-16T12:00:00.000Z","observed_at":"2026-09-16T12:00:01.000Z","revision":2,"has_results":true,"dispatched_at":"2026-09-16T12:00:00.000Z","elapsed_ms":1000,"http_status":202,"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nThis detail route accepts no query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nFresh computer scope or the activity store could not be read; the response includes incomplete:true.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/activities/act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    activity = \"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7\"\n    c = client.computers.get(id)\n    result = c.activity(activity)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst activity = \"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7\";\nconst c = await client.computers.get(id);\nconst result = await c.activity(activity);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read one API activity (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetActivity","tags":["Activities"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"activity","in":"path","required":true,"schema":{"type":"string"},"description":"An `activity_id` from `GET /computers/{id}/activities` (`act_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Safe activity metadata only; never raw execution content. Responses are no-store.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nThis detail route accepts no query parameters.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nFresh computer scope or the activity store could not be read; the response includes incomplete:true.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdActivitiesByActivity","tags":["Activities"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"activity","in":"path","required":true,"schema":{"type":"string"},"description":"An `activity_id` from `GET /computers/{id}/activities` (`act_` followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/activities/{activity}/results":{"get":{"summary":"Read passive activity result links","description":"Member-only metadata for at most eight newest retained versions with a ninth-row lookahead and more flag. No query/body. One current daemon scope proof covers sequential file-metadata verification; original authority, history revision and result generations are checked again before return. Historical activity visibility does not authorize a result from another recorded scope. No guest output/file/exec calls, wake, content reads or new capture. Replies are at most 8192 bytes; items at most 896 bytes. An existing activity without versions returns an empty list; missing history is 404; unavailable owner/history/current scope is 503. Results use exact execution identity, a trusted synchronous request token, or verified caller-selected artifact association; no PID/time/path inference. Artifacts do not imply creation or execution success. Late links advance activity revision without rewriting observed_at.\n\nRequires the `member` role or higher.","operationId":"getActivityResults","tags":["Activities"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"activity","in":"path","required":true,"schema":{"type":"string"},"description":"An `activity_id` from `GET /computers/{id}/activities` (`act_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Finite references and availability/truncation metadata only; no URLs, hashes, output or paths. No-store.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivityResults"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"activity_id":"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7","revision":2,"more":false,"items":[{"id":"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6","kind":"background-output","association":"background_execution_output","availability":"available","captured_at":"2026-09-16T12:00:01.000Z","expires_at":"2026-09-17T12:00:00.000Z","stdout":{"bytes":20,"retained_truncated":false,"upstream_truncated":null},"stderr":{"bytes":0,"retained_truncated":false,"upstream_truncated":null},"diagnostic":{"bytes":0,"truncated":false},"observation":{"status":"exited","exit_code":0}}]}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nThe activity history, current scope or local backing storage cannot be verified, the history changed while it was being read, or the read ran out of time (`activity_results_unavailable`). It can be sent again. A linked result or artifact whose stored file or record fails its check is withdrawn: a `200` lists it with `availability: \"unavailable\"`, and reading it directly answers `404`. This `503` may come before or after such a withdrawal, so it says nothing about any one item: send the read again, or read the item directly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/activities/act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7/results' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    activity = \"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7\"\n    c = client.computers.get(id)\n    result = c.activity_results(activity)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst activity = \"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7\";\nconst c = await client.computers.get(id);\nconst result = await c.activityResults(activity);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read passive activity result links (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetActivityResults","tags":["Activities"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"activity","in":"path","required":true,"schema":{"type":"string"},"description":"An `activity_id` from `GET /computers/{id}/activities` (`act_` followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Finite references and availability/truncation metadata only; no URLs, hashes, output or paths. No-store.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso `result_capacity` with `Retry-After: 60` when 4 reads of retained results and artifacts are already in flight for this account, or 32 across the platform; a read is refused rather than queued.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nThe activity history, current scope or local backing storage cannot be verified, the history changed while it was being read, or the read ran out of time (`activity_results_unavailable`). It can be sent again. A linked result or artifact whose stored file or record fails its check is withdrawn: a `200` lists it with `availability: \"unavailable\"`, and reading it directly answers `404`. This `503` may come before or after such a withdrawal, so it says nothing about any one item: send the read again, or read the item directly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdActivitiesByActivityResults","tags":["Activities"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"activity","in":"path","required":true,"schema":{"type":"string"},"description":"An `activity_id` from `GET /computers/{id}/activities` (`act_` followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/input":{"post":{"summary":"Send input","description":"Moves the pointer, clicks, drags, scrolls, types or presses keys.\n\nThree request shapes are accepted. The flat one below is this API’s own. The second is an Anthropic computer-use `tool_use.input` block posted verbatim — `coordinate: [x, y]`, `start_coordinate`, `scroll_direction`, `scroll_amount`, `duration`, and `text` doing double duty as either the string to type or the modifier keys to hold — so a model’s tool call can be forwarded without translation.\n\nThe third is an OpenAI action with `type` instead of `action`: `click`, `double_click`, `drag`, `move`, `scroll`, `keypress`, `type`, `wait` or `screenshot`. Send one object or an array of 1–32 OpenAI action objects. Do not mix the two discriminators or dialect-specific fields. OpenAI pointer actions require both x and y, including zero. Click buttons are left, right, wheel (middle), back and forward. A drag path has 2–128 `{x,y}` points and holds one button through every waypoint. Coordinates are truncated to pixels, bounded to ±10,000,000, then clamped to the screen. OpenAI scroll_x/scroll_y are approximated as wheel notches: round each absolute distance divided by 100, with a minimum of one for a nonzero delta and zero for zero. Each axis is bounded to ±5000 pixels and their combined converted count to 50 notches; vertical then horizontal. Actual movement depends on the application. Optional keys (at most 16) are held around the entire OpenAI pointer gesture, including positioning, and released on failure. Arrow names, META/COMMAND and OPTION map to desktop keys. OpenAI wait lasts one second. OpenAI screenshot acknowledges a marker with ok:true; retrieve image bytes using GET /screenshot after the action(s). Duplicate or unused OpenAI fields, even zero-valued ones, are refused before auto-resume. The following alias/default rules apply to the two legacy action formats.\n\nA batch is fully validated before auto-resume or input. Combined limits are 400 typed Unicode characters, 50 wheel notches, 128 drag points and 10 one-second waits. The whole batch shares one input lane, one QMP connection when needed, and at most 75 seconds of work including cleanup, plus two five-second queues. QMP remains held during waits in a batch containing pointer or keyboard input; wait-only batches do not hold it. VNC and native guest applications are not serialized by this API lock. Execution stops at the first error. completed and results describe finished actions; failed_index is zero-based. The failing action may have partially applied. Inspect a fresh screenshot before retrying; never automatically replay the batch. Static validation/admission failures use the normal error response. A screenshot-only batch acknowledges markers without waking the computer. Optional context is read once after a successful batch. The proxy allows 93 seconds, including context and transport.\n\nCoordinates are absolute, in the screen space `resolution` reports.\n\nKeys can be held down for the length of a click, a scroll or a drag — ctrl-click, shift-click, shift-drag to extend a selection. Give them as `keys` (`[\"ctrl\", \"shift\"]`), or as a chord string in `key` or `text` (`\"ctrl+shift\"`), once. On a click or a drag they are pressed before the pointer moves and released after the button. On a scroll with a coordinate the pointer moves FIRST, with no keys held, and the keys are then pressed for the wheel notches and released after them. Either way they are released when the action fails part way.\n\nA body is read strictly. A field this endpoint does not have, a field the action does not read (`keys` on a `move`, a `coordinate` on a `type`, `button` on a click), the held keys given in more than one field, and an unknown `action` each answer 400 naming what was wrong, before anything is sent to the computer and before a suspended one is resumed. A computer tool’s `screenshot` and `zoom` are not input: posted here, with or without `region`, they answer 400 with `code: not_an_input_action` and say where each is served. Take a screenshot from `GET /computers/{id}/screenshot`, and serve a zoom as a crop of the screenshot the model is looking at: `?capture=` set to that screenshot’s `X-GC-Capture` header and `?region=x0,y0,x1-x0,y1-y0` from the tool’s `[x0, y0, x1, y1]`, in that screenshot’s pixels. `computer_toolset_20260801` turns zoom on unless its `configs` turn it off, and a model may still call it, so a harness forwarding tool calls verbatim should route `zoom` itself. It should also spend a `key` call’s `repeat` as one input per press (`repeat` is not a field here), and split a `wait` longer than 30 seconds. A field at its zero value — `\"\"`, `[]`, `0` — counts as not sent, except `x` and `y`, where `0` is a position: `{\"action\":\"key\",\"key\":\"ctrl+c\",\"x\":0,\"y\":0}` is a 400 (`key does not take x`). Leave them out, or send `null`.\n\n`amount` (or `scroll_amount`) is 1 to 50 notches, 3 when left out or not above 0; more than 50 is a 400. `duration` is at most 30 seconds, 1 second when left out or not above 0; longer is a 400. Those two, a scroll direction other than `up`, `down`, `left` or `right`, and a key name this endpoint does not know — in a chord or held during a pointer action — are checked with the rest of the body: a 400 before a suspended computer is resumed, and the same 400 from a stopped one.\n\n`type` requires non-empty valid Unicode text, at most 400 characters. Plain ASCII is typed as physical key events, about 12 ms a character (each key held 8 ms, then 4 ms before the next). Text containing any other character is typed WHOLE, in request order, by one guest helper: its ASCII at the same physical key positions and shift levels, and every other character by GTK Unicode composition. This works in supported Chromium GTK3 and Xfce Terminal/VTE configurations on Linux X11, including accents, CJK, emoji and combining sequences, without changing the clipboard or keyboard mapping. Tab and LF are keys; CRLF sends one Return. Bare CR and other C0/C1/DEL controls are refused. Malformed or unsupported text is checked before auto-resume or any keys. Unicode typing requires XTEST, XRes process identification, Python Xlib, GTK3, timeout, a neutral supported US keyboard mapping and an effective gtk-im-context-simple input context. A no-key preflight verifies the supported application/configuration before even a mixed request’s ASCII prefix. Firefox, other applications, GTK4, custom input modules, disabled GTK IME, Windows and native Wayland Unicode typing are unsupported. Guest configuration is not changed. The response reports `mechanism: physical | unicode | mixed`. Use exclusive desktop control and verify the resulting text: delivery does not prove application acceptance. Runtime failure or cancellation can leave partial text or composition state. Cancellation waits for the bounded in-flight helper; cleanup never sends Escape into uncertain focus. Unicode requests share 75 seconds for all work including cleanup, with a 95-second proxy allowance for queues and transport; inspect before retrying and never automatically replay.\n\nFor fast text insertion use `{\"action\":\"paste\",\"text\":\"Café — 東京 😀\"}`. This writes the Linux desktop clipboard and sends Ctrl+V. Use `key: \"ctrl+shift+v\"` for terminals that require it, Shift+Insert is refused because it can read the separate primary selection. Text must be non-empty UTF-8 without NUL, at most 8192 bytes. Windows and guests without working clipboard support are refused.\n\nPaste replaces the clipboard and leaves it in place. API input/clipboard writes are serialized through the shortcut, but native guest applications and VNC clients can still change it. Use exclusive desktop control and wait for the target to consume the paste before changing the clipboard again. A 200 means the clipboard write and shortcut were delivered, not that the application accepted or inserted the text. Check the target afterward; paste-disabled fields may ignore it. An interrupted request can have side effects: inspect before retrying, and do not automatically fall back to typing or replay it.\n\n`count` repeats a `left_click`, `right_click` or `middle_click` up to 10 times, for the applications that read a click count past three.\n\nAdd `?context=1` to have the answer carry the desktop’s windows and the focused one as they stand just after the action — on any action, `wait` included. A query parameter other than `context`, one given twice or empty, and a `context` other than `1`, `true`, `0` or `false` each answer 400 before anything is sent to the computer.\n\nRequires the `member` role or higher.","operationId":"sendInput","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"context","in":"query","required":false,"schema":{"type":"string","enum":["1","true","0","false"]},"description":"Answer the desktop’s windows after the action — any action, `wait` included — as `context`, saving the round trip to `GET /computers/{id}/windows`, and, when the focused window is Chromium, the page on screen as `context.dom`. Costs one windows read and at most one page read, bounded at 3 seconds together, after the action. If the windows cannot be read the action still answers `ok: true`, with `context_error` saying why and no `context`; if only the page cannot (any other window focused included), `context` has no `dom` and `context_error` says why. `1` or `true` asks; `0`, `false` or leaving it out does not."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"action":{"type":"string","description":"One of `move`/`mouse_move`, `left_click`, `right_click`, `middle_click`, `double_click`, `triple_click`, `left_mouse_down`, `left_mouse_up`, `left_click_drag`, `scroll`, `type`, `paste`, `key`, `hold_key`, `cursor_position`, `wait`."},"x":{"type":"integer","description":"Absolute. Omit to act where the pointer already is, except on `move`/`mouse_move` and `left_click_drag` (whose destination it is): those need a coordinate, and one sent without it is refused with a 400 before a suspended computer is woken. `0` is a position, not an omission — except on `scroll`, where a flat `x: 0, y: 0` scrolls where the pointer is, and `coordinate: [0, 0]` is needed to scroll at the top-left corner."},"y":{"type":"integer"},"coordinate":{"type":"array","items":{"type":"number"},"description":"`[x, y]`, as an alternative to x/y."},"start_coordinate":{"type":"array","items":{"type":"number"},"description":"Where a `left_click_drag` starts."},"text":{"type":"string","description":"The string for `type` (non-empty, at most 400 Unicode characters) or `paste` (1–8192 UTF-8 bytes, no NUL). For `key` and `hold_key`, the chord. On a click, a scroll or a drag, the keys to hold down during it."},"key":{"type":"string","description":"A chord such as `ctrl+c`, for `key` and `hold_key`. On a click, a scroll or a drag, the keys to hold down during it. For `paste`: ctrl+v (default) or ctrl+shift+v."},"keys":{"type":"array","items":{"type":"string"},"description":"The chord as separate keys, for `key`, `hold_key` and `paste`. On a click, a scroll or a drag, the keys to hold down during it."},"button":{"type":"string","description":"Scroll direction: `up`, `down`, `left`, `right`."},"scroll_direction":{"type":"string","description":"The computer-use spelling of `button`."},"amount":{"type":"integer","description":"Scroll notches, 1 to 50; 3 when left out. More than 50 is a 400."},"scroll_amount":{"type":"integer","description":"The computer-use spelling of `amount`."},"duration":{"type":"number","description":"Seconds, for `wait` and `hold_key`: at most 30, and 1 when left out. Longer is a 400."},"count":{"type":"integer","description":"How many times `left_click`, `right_click` or `middle_click` presses its button, 1 to 10; 1 when left out. The presses are paced as `double_click`’s are, so the guest sees one multi-click. More than 10, or below 1, is a 400, and so is `count` on `double_click` or `triple_click`, which are two and three clicks by name."}},"required":["action"]},{"$ref":"#/components/schemas/OpenAIInput"},{"type":"array","minItems":1,"maxItems":32,"items":{"$ref":"#/components/schemas/OpenAIInput"}}]}}}},"responses":{"200":{"description":"Delivered. `type` reports its mechanism; `cursor_position` answers where the pointer is and whether that is known. Verify application acceptance.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/InputResult","not":{"required":["completed"]}},{"$ref":"#/components/schemas/InputBatchResult"}]},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["completed"]}},{"$ref":"#/components/schemas/InputBatchResult"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["completed"]}},{"$ref":"#/components/schemas/InputBatchResult"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["completed"]}},{"$ref":"#/components/schemas/InputBatchResult"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["completed"]}},{"$ref":"#/components/schemas/InputBatchResult"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/input' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"action\":\"left_click\",\"x\":640,\"y\":400}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"action\\\":\\\"left_click\\\",\\\"x\\\":640,\\\"y\\\":400}\")\n    c = client.computers.get(id)\n    result = c.click(body[\"x\"], body[\"y\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"action\":\"left_click\",\"x\":640,\"y\":400};\nconst c = await client.computers.get(id);\nconst result = await c.click(body.x, body.y);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdInput","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/exec":{"post":{"summary":"Run a command","description":"Runs a command inside the guest and waits for it, up to `timeout_s`.\n\nOn the hosted API a foreground request is cut off at the edge after about 120 seconds, whatever `timeout_s` says (a `524`). The command keeps running, and until `timeout_s` runs out — or the command ends, if that is sooner — it holds the computer’s guest agent, so another exec, a clipboard, window or file call on that computer can answer 409 in that time, and so can input that goes through the guest agent: a `paste`, or a `type` whose text is not plain ASCII. Screenshots, pointer and key input, and a `type` of plain ASCII text do not wait on it. Use `background: true` for anything that may take more than about 100 seconds.\n\nPass `background: true` for a command that outlives the request: the response is a handle carrying the guest pid, and you read its output with `GET /computers/{id}/exec/{pid}` and stop it with the DELETE. `timeout_s` means nothing alongside it, because not waiting is the whole request. One computer may hold at most 16 background commands. A seventeenth answers 409 without a `reason`: the existing commands may be long-lived servers, so retrying does not necessarily help; stop one of the handles you already hold first.\n\nA stopped computer answers 409 with `reason: \"unavailable\"`, a foreground command or a background one: start it first. Waiting does not clear it. A computer that stops while its command is starting or running answers the same, and that command may have run in part, so decide for yourself whether to send it again. A computer being moved to another host answers 409 with `reason: \"contention\"` instead, and the same request works once the move finishes. A command the move itself cuts off answers 409 `unavailable`, as any other stop does: it may have run in part, so it is not one to send again without deciding to.\n\nA suspended computer is resumed to run the command — after `session`, `env` and `cwd` have been checked, so a request refused for one of them, or for asking a Windows guest for a desktop or background command, never resumes or bills the computer.\n\nExplicit retain_output:true or a finite options object captures only the already-returned completed synchronous response. Missing/false performs no retained storage work. The option is public/internal only; it does not open session exec. Effective background:true with retention, duplicate canonical options, or trailing non-whitespace after an enabled first object is invalid_retain_output 400 before dispatch. Original request bytes are forwarded unchanged. Optional admission adds at most 1000 milliseconds and publication at most 5000 milliseconds; inspection is bounded at 50331648 response bytes. Only successful immutable publication adds result_id. Every optional failure preserves the original command status/body with X-GC-Retained-Output: unavailable. Missing/false adds no header. Original credential loss takes precedence over private delivery; a refused or lost response does not prove the command did not run. Never replay exec to repair retention.\n\nRequires the `member` role or higher.","operationId":"runCommand","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"command":{"type":"string","description":"The command line."},"session":{"type":"string","description":"Empty runs as the guest agent’s own account — root, or SYSTEM on Windows. `desktop` runs inside the console user’s graphical session, which is where a GUI application has to start: launch one any other way and it has no display to appear on.\n\n`desktop` is refused outright on Windows guests — there is no way to reach the interactive session there yet — and the refusal carries `reason: \"unsupported\"`, the word for something no action on the computer changes. It is answered before the computer is asked whether it is running, so a stopped Windows computer tells you this rather than telling you to start it first. Empty `session` is unaffected: an ordinary command runs on Windows through `cmd.exe`.\n\nOn Linux, a computer made from an older image has a shell profile hook that would run its bound secrets as code, and the platform replaces it before the first `desktop` command. If that replacement fails (a full or read-only guest disk, say), `desktop` answers `503` with no `reason` and nothing is run; the next request tries the replacement again. An empty `session` runs as root, which that hook skips, and is never refused for it."},"timeout_s":{"type":"integer","description":"Seconds to wait. Defaults to 30, and 600 is the most it accepts — the wait holds the computer’s guest agent, so no other call that needs it reaches that machine for its length, and a higher value is a `400` rather than a quietly shorter wait. Send `background: true` for anything longer. Ignored when `background` is set. On the hosted API the edge cuts a foreground request off after about 120 seconds whatever this says. The command keeps running and holds the guest agent until this runs out, or until it ends if that is sooner, so exec, clipboard, window and file calls on that computer can answer 409 until then, as can a `paste` input or a `type` whose text is not plain ASCII: use `background: true` for anything that may take more than about 100 seconds."},"background":{"type":"boolean","description":"Answer with a handle instead of a result. Refused on Windows guests with `400` and `reason: \"unsupported\"`, before the computer is resumed."},"retain_output":{"$ref":"#/components/schemas/RetainOutput"},"cwd":{"type":"string","description":"Working directory inside the guest: an absolute path, or the request is a `400`. If the shell cannot enter it, no part of the command runs and `exit_code` is 127, on Linux and on Windows alike. On Windows a UNC path, or one containing `\"`, `%` or `!`, is also a `400`, and the command runs under `cmd.exe` as `(cd /d \"<cwd>\" || exit 127) & <command>`, so a compound command is parsed as it would be on its own. `cmd.exe` expands `%VAR%` across that whole line before any of it runs, so a `%CD%` in the command reads the directory `cmd.exe` started in, not `cwd`. Whether a Windows guest enters `cwd` this way has not yet been measured: if it does not, every such command exits 127 without running. A 127 alone does not prove that, since a command that ran can exit 127 too."},"env":{"type":"object","additionalProperties":{"type":"string"},"description":"Environment variables. At most 64 entries, each at most 4096 bytes as `NAME=value` — the name, the value and the `=` between them count together, so a long value leaves less room for a long name. Past either is a 400, and so are an empty name, an `=` inside a name, and a NUL in either half — the last two because the guest agent would carry them silently rather than refuse. The per-entry refusals name the entry they mean; going over the COUNT reports the two counts, since there is no one entry at fault. Nothing partial runs. These bound what the guest agent is asked to spawn, so they apply whether or not `background` is set.\n\nWhat these are added TO is worth saying, because it is not what the guest agent was started with. On Linux your command runs through a login shell, so the base is whatever the guest user’s profile sets — `PATH` included — and your entries go on top of that. A variable the guest agent happened to hold and the profile does not set is not there to inherit. On Windows a command runs through `cmd.exe`, which sources no profile, so there is no such base; `background` is refused there outright.\n\nSecrets bound to the computer as environment variables are in that base too, on an image that supports it, for every command — a plain one run as root as well as one with `session: \"desktop\"` — at their values when the command starts, so a replaced value applies to the next command. Windows computers cannot be bound to secrets.\n\nAn entry here wins over both: over a bound value of the same name, and over whatever a login profile sets while the shell starts (a template’s defaults, `PATH`), because your entries are applied again, as plain strings, after the profiles and the bound values, just before your command. Not applied again: a name a shell cannot assign or that it uses itself (such as `PS4`, or anything beginning `BASH_`), which keeps what the shell made of it; a variable a profile made read-only; and, with `session: \"desktop\"`, the session’s own `DISPLAY`, `XAUTHORITY`, `DBUS_SESSION_BUS_ADDRESS`, `XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, `HYPRLAND_INSTANCE_SIGNATURE`, `HOME`, `USER` and `LOGNAME`, which stay the session’s: a session that has one keeps its value, and one it does not have is set only by your entry, if you give one, and otherwise unset.\n\nYour entries are ALSO in the environment the login profiles run under, from the moment the shell starts: they are applied again after the profiles, not instead of being there during them. So overriding `PATH`, or anything else a profile reads, changes what the profiles do, and can break one; a profile that fails that way adds its errors to the command’s `stderr`."}},"required":["command"]}}}},"responses":{"200":{"description":"The command finished inside `timeout_s`. This is its result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Exec"},"examples":{"completed":{"summary":"Completed synchronous command result.","value":{"exit_code":0,"stdout_b64":"dG90YWwgNAotcnctci0tci0tIDEgdXNlciB1c2VyIDIwIFNlcCAxNiAxMjowMCBub3Rlcy50eHQK","stderr_b64":"","timed_out":false,"out_truncated":false,"err_truncated":false}}}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"202":{"description":"The command was started and is still running. This is the handle: read its output with `GET /computers/{id}/exec/{pid}` and stop it with the DELETE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecHandle"},"examples":{"background":{"summary":"Background command accepted; this handle is not a completed result.","value":{"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","pid":4242,"command":"sleep 30; printf \"Hello from Mandala.\\n\"","running":true,"exited":false,"stdout_b64":"","stderr_b64":"","stdout_offset":0,"stderr_offset":0,"started_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Retained-Output":{"description":"Only when `retain_output` asked for retention: `unavailable` when no retained result was published, so the body is the command’s own answer without a `result_id`. Absent when one was published, and whenever retention was not asked for.","schema":{"type":"string","enum":["unavailable"]}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/exec' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"command\":\"ls -la /home/user\",\"timeout_s\":30}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"command\\\":\\\"ls -la /home/user\\\",\\\"timeout_s\\\":30}\")\n    c = client.computers.get(id)\n    result = c.exec(body[\"command\"], timeout=body[\"timeout_s\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"command\":\"ls -la /home/user\",\"timeout_s\":30};\nconst c = await client.computers.get(id);\nconst result = await c.exec(body.command, {timeoutS: body.timeout_s});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdExec","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/browser-connections":{"post":{"summary":"Open a browser connection","description":"Launch or reuse a dedicated Chromium profile in the desktop session of a running Linux computer. Send {} for the legacy ten-minute grant, or lifecycle_version: 2 for a renewable session. Requires the guest streaming channel and Chromium. Returns a CDP WebSocket capability, with at most eight grants per computer and one active socket per grant. Use the returned URL with an Authorization: Bearer header containing the returned token, spelled exactly `Authorization: Bearer <token>`: `Bearer` capitalised, one space, and nothing after the token. Other spellings the REST API accepts are a `401` here. Account API keys, desktop tokens and query-string tokens do not authenticate this socket. Legacy expiry closes the socket and permits reconnect before expiry. Version 2 keeps expires_at as an immutable alias of attach_expires_at: attach once, before the shorter of ten minutes and lease_seconds. Its active lease defaults to 1800 seconds and the absolute session cap is 7200 seconds from creation. Renew the existing attached connection with POST /computers/{id}/browser-connections/{connection}/renew using account authorization. No independent idle timeout applies. Version 2 disconnect revokes the grant; renewal never extends the attachment credential or reconnects Chromium. DELETE closes either version without stopping the browser. The managed profile persists on disk and is shared by grants for this computer; it is separate from the default desktop profile. A stop, restart, suspend, migration, desktop-credential rotation or daemon restart invalidates the grants. Issued capabilities are delegated access: revoking the issuing API key does not itself revoke them; delete the grant or wait for expiry. Raw CDP grants full control of that browser, including JavaScript and guest file access; it is not a read-only or tab-scoped capability. WebSocket reconnect never replays commands. Clients must decide whether an interrupted action completed. The endpoint is Chromium CDP, not the Playwright server protocol.\n\nThe WebSocket handshake answers `101` when it attaches. It answers `400` when a `GET` whose token the host accepts is not a WebSocket upgrade; `401` for any other method, a missing, expired or revoked token, a query string on the URL, a version 2 grant that has already attached once, or a computer or account that is no longer active: a computer deleted, being deleted or written off with its host, or a suspended account. A computer whose host is only unreachable is still active; see the `503` below for what it gets. `409` when a legacy grant already has an open socket, or another handshake with the same token is still under way; `502` when the browser in the guest cannot be reached, so request a new connection; and `503` when the computer’s host cannot be found or does not answer and the host accepted this token on a handshake within the last ten minutes. That `503` says only that the token cannot be checked now. Once the host answers, a legacy grant that has not expired, or a version 2 grant that never attached, is answered on its merits; a version 2 token that has attached is spent and gets the `401`, so request a new connection. A token the host has not accepted within the last ten minutes cannot be checked while the host is unreachable and gets the `401`; if you just created it, read the computer or request a new connection, which say whether its host is reachable. Under heavy load, or after many refused attempts from your address within a minute, it can also answer `503` with `Retry-After` instead of judging the token: send the same token again after that many seconds and it is answered on its merits, `101` if it is good and `401` if not. A `401` is never about load.\n\nRequires the `member` role or higher.","operationId":"createBrowserConnection","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lifecycle_version":{"type":"integer","const":2,"description":"Opt in to renewable leases. Omit all options for legacy behavior."},"lease_seconds":{"type":"integer","minimum":60,"maximum":7200,"default":1800,"description":"Requires lifecycle_version: 2. Must not exceed max_duration_seconds. The default is not lowered to fit: with a max_duration_seconds below 1800, send lease_seconds too, or the request is a 400."},"max_duration_seconds":{"type":"integer","minimum":60,"maximum":7200,"default":7200,"description":"Requires lifecycle_version: 2. Absolute lifetime from creation; renewal cannot extend it."}},"additionalProperties":false}}}},"responses":{"201":{"description":"A browser connection and its expiry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrowserConnection"},"examples":{"success":{"summary":"A short-lived browser capability. The token is illustrative.","value":{"id":"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","url":"wss://app.mandala.computer/api/v1/computers/vm-a1b2c3d4e5f6/browser-connections/a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4/cdp","token":"bcdp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","expires_at":"2026-10-09T00:10:00Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\n`reason` says whether waiting helps. `unsupported`: a Windows computer, or a running or suspended one whose hardware lacks the guest streaming channel because it was last started before interactive terminals existed (a stop and a start adds it), or one whose process its host cannot read until support repairs the host; sending the same request again never helps. Also `unsupported` when the computer already holds eight grants: revoke a connection with `DELETE` before opening another. A grant can otherwise hold its slot until its expiry, up to two hours. `contention`: the computer changed while the browser was opening, its host could not read whether it is running just now, its guest agent is busy with another operation (a snapshot’s capture among them) or another terminal connection is being set up on it, or a suspend has begun; send it again — after a suspend, the answer is the `unavailable` below. `starting`: the guest agent has not answered yet inside its boot window, as while the computer boots or restarts; send it again in a few seconds. `unavailable`, with `resume_required: true` when it is suspended: the computer is not running, or stopped while the browser was opening, so start it — a stopped one without the channel gets it from that cold start. No `reason` when the guest has no active desktop session, as on the other desktop routes: a desktop still starting looks the same as one nobody is signed in to. The WebSocket handshake’s own `409` carries `contention`. On a legacy grant it means another socket is open or opening, and it clears when that socket closes, if the grant has not expired by then. On a version 2 grant it means another handshake with the same token is still under way: if that one attaches, the token is spent and gets `401`; if it does not, the token can attach again before `attach_expires_at`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThe options body is over 1024 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.\n\nThe browser could not be opened: the launcher in the guest failed or answered with no usable endpoint, the browser transport could not be prepared, or the guest agent is still not answering past its boot window. No `reason`, because none of these clears by waiting; a guest that is still booting, busy or being suspended is the `409` above instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso the answer to `lifecycle_version: 2` on a computer whose host does not support renewable sessions yet (`Renewable browser sessions are not available on this computer yet.`). No token is returned and the grant the host opened is revoked. If it cannot be revoked, the message says so: that grant holds one of the computer’s eight connections until it expires, within ten minutes, and nobody can use it. Send `{}` for a legacy grant, or try again later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/browser-connections' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.create_browser_connection()\n    print(\"Token returned once; store result.token securely without logging it.\")"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.createBrowserConnection();\nconsole.log(\"Token returned once; store result.token securely without logging it.\");"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdBrowserConnections","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/browser-connections/{connection}":{"delete":{"summary":"Revoke a browser connection","description":"Immediately close the socket and revoke this computer’s named browser capability. Idempotent for an absent grant. Does not close Chromium or erase its managed profile.\n\nRequires the `member` role or higher.","operationId":"revokeBrowserConnection","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"connection","in":"path","required":true,"schema":{"type":"string"},"description":"The connection id from POST /computers/{id}/browser-connections."}],"responses":{"200":{"description":"The grant is revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"The capability has been revoked.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/browser-connections/a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    connection = \"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\"\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.revoke_browser_connection(connection)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst connection = \"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\";\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.revokeBrowserConnection(connection);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdBrowserConnectionsByConnection","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"connection","in":"path","required":true,"schema":{"type":"string"},"description":"The connection id from POST /computers/{id}/browser-connections."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/browser-connections/{connection}/renew":{"post":{"summary":"Renew an attached browser session","description":"Set a version 2 session’s lease_expires_at to the earlier of now plus its lease_seconds and the original absolute_expires_at; renewal never shortens it. Requires fresh member-level account authorization and the same computer generation. The browser capability cannot authorize renewal. Expired, revoked, legacy, unattached or disconnected sessions return 409; they are never recreated. At the absolute cap, a still-active lease can return the unchanged deadline. No browser action is replayed. Revoking an account API key or membership prevents renewal; existing delegated access remains valid only until its current lease ends or is explicitly revoked.\n\nRequires the `member` role or higher.","operationId":"renewBrowserConnection","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"connection","in":"path","required":true,"schema":{"type":"string"},"description":"The connection id from POST /computers/{id}/browser-connections."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"responses":{"200":{"description":"Updated deadlines for the same connection; no replacement token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrowserSessionLease"},"examples":{"success":{"summary":"The same browser session lease is extended.","value":{"id":"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","lifecycle_version":2,"server_time":"2026-10-09T00:20:00Z","attach_expires_at":"2026-10-09T00:10:00Z","lease_expires_at":"2026-10-09T00:50:00Z","absolute_expires_at":"2026-10-09T02:00:00Z","lease_seconds":1800,"idle_timeout_seconds":0}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\n`reason: \"contention\"` while this connection’s WebSocket handshake is still in progress: renew it once the socket is open. Otherwise `reason: \"unsupported\"`: the session is not an active renewable one. It expired, was revoked or disconnected, was invalidated by a stop, restart, suspend, migration or desktop-credential rotation, is a legacy grant, or has not attached. Waiting never changes that answer and the session is never recreated: open a new connection, or attach this one first if it has not.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThe body is over 1024 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/browser-connections/a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4/renew' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    connection = \"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\"\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.renew_browser_connection(connection)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst connection = \"a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\";\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.renewBrowserConnection(connection);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdBrowserConnectionsByConnectionRenew","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"connection","in":"path","required":true,"schema":{"type":"string"},"description":"The connection id from POST /computers/{id}/browser-connections."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/exec/{pid}":{"get":{"summary":"Read a background command","description":"What a backgrounded command has printed since you last asked, and whether it is still running.\n\nThe read is CONSUMING and the cursor is the daemon’s, not yours: each call returns what has arrived since the previous call and advances the handle’s offset itself. There is nothing to pass — the `stdout_offset` and `stderr_offset` on the response are how far it has now read, reported so you can tell how much has gone by, not a parameter to send back. Output you receive and drop is gone.\n\n`stdout_b64` and `stderr_b64` are base64 and always are; the offsets count the DECODED bytes, which is what makes them line up across polls.\n\n`more` is the field to poll on: it says there is further output waiting right now.\n\nA handle answers `404` from ten minutes after its command exits, and after the daemon on the computer’s host restarts — the same `404` as a pid that never existed. A stopped or suspended computer answers `409` with `reason: \"unavailable\"`, and this route does not resume it. A suspended one still holds the command: after a resume, its output and exit code can be read again.\n\nOnly commands this API started can be read this way.\n\nRequires the `member` role or higher.","operationId":"readCommand","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"pid","in":"path","required":true,"schema":{"type":"string"},"description":"The guest pid from a backgrounded `exec`."}],"responses":{"200":{"description":"Its state and whatever it has printed since the last read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecStatus"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","pid":4242,"command":"sleep 30; printf \"Hello from Mandala.\\n\"","running":false,"exited":true,"stdout_b64":"SGVsbG8gZnJvbSBNYW5kYWxhLgo=","stderr_b64":"","stdout_offset":20,"stderr_offset":0,"started_at":"2026-09-16T12:00:00.000Z","exit_code":0}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/exec/4242' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    pid = \"4242\"\n    c = client.computers.get(id)\n    result = c.background_command(int(pid)).poll()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst pid = \"4242\";\nconst c = await client.computers.get(id);\nconst result = await c.execPoll(Number(pid));\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read a background command (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. This one checks metadata instead of reading the content: it never resumes a guest, consumes output or opens a stream, so a header or failure that only reading the content could produce may be missing.\n\nRequires the `member` role or higher.","operationId":"headReadCommand","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"pid","in":"path","required":true,"schema":{"type":"string"},"description":"The guest pid from a backgrounded `exec`."}],"responses":{"200":{"description":"Its state and whatever it has printed since the last read.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"delete":{"summary":"Stop a background command","description":"Stops a backgrounded command and answers the handle as it was when it stopped, with whatever it had printed and not yet been read.\n\nThe signal goes to the PROCESS GROUP, so a command that started children takes them with it: `TERM` first, then `KILL` if it is still there when the grace period ends. `killed` on the answer means this call did the stopping — including when the command had in fact already ended and nothing had noticed yet, so do not read it as \"your command was cut short\". See the field.\n\nA handle answers `404` from ten minutes after its command exits, and after the daemon on the computer’s host restarts — the same `404` as a pid that never existed. A stopped or suspended computer answers `409` with `reason: \"unavailable\"`, and this route does not resume it. A suspended one still holds the command: after a resume, its output and exit code can be read again.\n\nOnly commands this API started can be stopped this way, and a `{pid}` that is not a number is a `400` rather than a 404.\n\nRequires the `member` role or higher.","operationId":"stopCommand","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"pid","in":"path","required":true,"schema":{"type":"string"},"description":"The guest pid from a backgrounded `exec`."}],"responses":{"200":{"description":"The handle as it was when the command was stopped.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecStatus"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","pid":4242,"command":"sleep 30; printf \"Hello from Mandala.\\n\"","running":false,"exited":true,"stdout_b64":"","stderr_b64":"","stdout_offset":0,"stderr_offset":0,"started_at":"2026-09-16T12:00:00.000Z","exit_code":143,"killed":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/exec/4242' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    pid = \"4242\"\n    c = client.computers.get(id)\n    result = c.background_command(int(pid)).kill()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst pid = \"4242\";\nconst c = await client.computers.get(id);\nconst result = await c.execKill(Number(pid));\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdExecByPid","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"pid","in":"path","required":true,"schema":{"type":"string"},"description":"The guest pid from a backgrounded `exec`."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/executions/{executionId}":{"get":{"summary":"Read execution metadata","description":"Reads daemon memory only, without guest commands, files, watcher setup or automatic resume. Only background exec participates; synchronous results have no retrievable execution ID yet. The ID comes from the 202 response and both exited/lost process.exited payloads. IDs cannot resolve to a replacement command when a PID is reused. Current computer scope and the original execution scope must both match. No command text or output is included. Handles are ephemeral: daemon restart, computer removal, PID replacement or cleanup makes an ID unavailable. Observed exits expire after ten minutes. Unknown and expired IDs both return 404 with code execution_unavailable, never an empty successful result. A computer outside the caller’s scope returns 404 computer not found with no code. Lost may be observed briefly before the handle is removed.\n\nRequires the `member` role or higher.","operationId":"getExecution","tags":["Control"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"executionId","in":"path","required":true,"schema":{"type":"string"},"description":"The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Last known daemon metadata; no guest work.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Execution"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","computer_id":"vm-a1b2c3d4e5f6","pid":4242,"status":"exited","started_at":"2026-09-16T12:00:00.000Z","ended_at":"2026-09-16T12:00:02.000Z","exit_code":0,"output_source":"volatile_guest_files"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/executions/exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    executionId = \"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\"\n    c = client.computers.get(id)\n    result = c.execution(executionId)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst executionId = \"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\";\nconst c = await client.computers.get(id);\nconst result = await c.execution(executionId);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read execution metadata (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetExecution","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"executionId","in":"path","required":true,"schema":{"type":"string"},"description":"The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters)."}],"responses":{"200":{"description":"Last known daemon metadata; no guest work.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdExecutionsByExecutionId","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"executionId","in":"path","required":true,"schema":{"type":"string"},"description":"The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/executions/{executionId}/output":{"get":{"summary":"Read execution output independently","description":"Explicit guest-file reads with caller-owned byte offsets. No shared cursor or diagnostic is consumed. Retry the same offsets after a lost response; reads never replay a command or automatically resume the computer. A stopped or suspended computer returns 409. A missing log returns 409 with code output_unavailable, not empty output. Unknown, expired, replaced or lost handles return 404 with code execution_unavailable; a computer outside the caller’s scope returns 404 computer not found with no code. Empty success means the existing files had no bytes at the supplied positions. This route reads volatile, mutable guest paths: deletion, truncation, replacement or tampering can change bytes between retries. It is not suitable for passive Activities/history; explicitly capture retained-output for immutable later reads. Each query key may appear once. Offsets and limit must be nonnegative decimal integers; each offset plus limit must be at most 9007199254740991. Responses are no-store.\n\nRequires the `member` role or higher.","operationId":"readExecutionOutput","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"executionId","in":"path","required":true,"schema":{"type":"string"},"description":"The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters)."},{"name":"stdout_offset","in":"query","required":true,"schema":{"type":"integer","minimum":0,"maximum":9007199254740990},"description":"Explicit stdout byte offset. Start at 0; offset + limit must remain a safe JavaScript integer."},{"name":"stderr_offset","in":"query","required":true,"schema":{"type":"integer","minimum":0,"maximum":9007199254740990},"description":"Explicit independent stderr byte offset. Start at 0."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":1048576,"default":65536},"description":"Maximum decoded bytes per stream, not including the separate 64 KiB diagnostic."}],"responses":{"200":{"description":"Bounded raw bytes and next independent offsets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExecutionOutput"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"execution_id":"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4","stdout_b64":"SGVsbG8gZnJvbSBNYW5kYWxhLgo=","stderr_b64":"","stdout_offset":20,"stderr_offset":0,"stdout_more":false,"stderr_more":false,"diagnostic_b64":"","diagnostic_truncated":false}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/executions/exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4/output?stdout_offset=0&stderr_offset=0' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    executionId = \"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\"\n    query = {\"stdout_offset\":\"0\",\"stderr_offset\":\"0\"}\n    c = client.computers.get(id)\n    result = c.execution_output(executionId, stdout_offset=int(query[\"stdout_offset\"]), stderr_offset=int(query[\"stderr_offset\"]))\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst executionId = \"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\";\nconst query = {\"stdout_offset\":\"0\",\"stderr_offset\":\"0\"};\nconst c = await client.computers.get(id);\nconst result = await c.executionOutput(executionId, {stdoutOffset: Number(query.stdout_offset), stderrOffset: Number(query.stderr_offset)});\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read execution output independently (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headReadExecutionOutput","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"executionId","in":"path","required":true,"schema":{"type":"string"},"description":"The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters)."},{"name":"stdout_offset","in":"query","required":true,"schema":{"type":"integer","minimum":0,"maximum":9007199254740990},"description":"Explicit stdout byte offset. Start at 0; offset + limit must remain a safe JavaScript integer."},{"name":"stderr_offset","in":"query","required":true,"schema":{"type":"integer","minimum":0,"maximum":9007199254740990},"description":"Explicit independent stderr byte offset. Start at 0."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":1048576,"default":65536},"description":"Maximum decoded bytes per stream, not including the separate 64 KiB diagnostic."}],"responses":{"200":{"description":"Bounded raw bytes and next independent offsets.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdExecutionsByExecutionIdOutput","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"executionId","in":"path","required":true,"schema":{"type":"string"},"description":"The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/windows":{"get":{"summary":"List windows","description":"What is on the desktop, as a list rather than a picture — id, title, class, type, geometry and which one has focus. Cheaper and more reliable than asking a model to find a window in a screenshot.\n\nA Windows computer answers `400` with `reason: \"unsupported\"`: listing windows is not supported on Windows guests yet. A stopped or suspended computer answers `409` with `reason: \"unavailable\"`, since this read does not resume one: start it first.\n\nRequires the `viewer` role or higher.","operationId":"listWindows","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["all"]},"description":"Include the desktop’s own furniture — panels, docks, the wallpaper window. Left out by default because none of it is a window a caller wants to act on."}],"responses":{"200":{"description":"The windows currently open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WindowList"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"windows":[{"id":"0x03e00007","title":"Terminal","class":"Xfce4-terminal","type":"normal","pid":3100,"x":40,"y":60,"width":900,"height":600,"focused":true,"visible":true}]}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/windows' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.windows()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.windows();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List windows (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListWindows","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["all"]},"description":"Include the desktop’s own furniture — panels, docks, the wallpaper window. Left out by default because none of it is a window a caller wants to act on."}],"responses":{"200":{"description":"The windows currently open.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdWindows","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/windows/{window}":{"post":{"summary":"Act on a window","description":"Focus, raise, minimize, maximize, unmaximize, close, move or resize one window. On a Wayland desktop a tiled window cannot be moved or resized — float it first — and an action the compositor refuses is a `400`. If the guest accepts the action but does not report its result before the deadline, this answers 504 without a `reason`. The action may already have happened, so do not treat that uncertain outcome as permission to repeat it. An image without the window tools (`xdotool` on an X11 desktop, `python3` on a Wayland one) answers `400` with `reason: \"unsupported\"`: listing its windows still works, and acting on them needs a computer created from an image that includes the tool. Do not retry it.\n\nA suspended computer is resumed to act on the window, after the action, the window id and the guest OS have been checked: a malformed action, or any action on a Windows guest, is refused without resuming or billing it.\n\nRequires the `member` role or higher.","operationId":"actOnWindow","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"window","in":"path","required":true,"schema":{"type":"string"},"description":"A window id from `GET /computers/{id}/windows`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","description":"`focus`, `raise`, `minimize`, `maximize`, `unmaximize`, `close`, `move` or `resize`."},"x":{"type":"integer","description":"Required for `move`, with `y`: -32768 to 32767. One without the other is a `400`."},"y":{"type":"integer","description":"Required for `move`, with `x`: -32768 to 32767."},"width":{"type":"integer","description":"Required for `resize`, with `height`: 1 to 32767. One without the other is a `400`."},"height":{"type":"integer","description":"Required for `resize`, with `width`: 1 to 32767."}},"required":["action"]}}}},"responses":{"200":{"description":"What the window is now, or that it has gone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WindowActed"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"window":{"id":"0x03e00007","title":"Terminal","class":"Xfce4-terminal","type":"normal","pid":3100,"x":40,"y":60,"width":900,"height":600,"focused":true,"visible":true},"gone":false}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/windows/0x3200007' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"action\":\"focus\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    window = \"0x3200007\"\n    c = client.computers.get(id)\n    result = c.window_action(window, \"focus\")\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst window = \"0x3200007\";\nconst c = await client.computers.get(id);\nconst result = await c.windowAction(window, \"focus\");\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdWindowsByWindow","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"window","in":"path","required":true,"schema":{"type":"string"},"description":"A window id from `GET /computers/{id}/windows`."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/clipboard":{"get":{"summary":"Read the desktop clipboard","description":"What is on the clipboard of the computer’s desktop — the `CLIPBOARD` selection, which is what Ctrl-C writes and Ctrl-V pastes.\n\nThis reads the selection out of the console user’s graphical session, so it needs no reboot and no permission from a browser — which is what makes it the route to build on if you want to write it once. The other way text crosses — RFB extended cut text over the desktop websocket — is faster and live, and needs a virtio-serial channel a computer only acquires on a COLD boot; see `Vnc.url`.\n\nIt does have ONE requirement of the image, and it is a 400 rather than something that clears: the guest needs `xclip` on an X11 desktop, or `wl-clipboard` (`wl-paste`/`wl-copy`) on a Wayland one. Every golden built since August 2026 carries it, so in practice this is a computer created before then — and a computer keeps the image it was created from, so the fix is to install that tool in the guest, which you can do yourself since you have root there, or to create a new computer from an image that includes it. The refusal says so in as many words, and carries `reason: \"unsupported\"`. **Do not retry it**: unlike the 409s below it will never start working.\n\nIt is a READ, not a subscription. Nothing here notices a Ctrl-C in the guest on its own, and this call does not resume a suspended computer: what somebody copied is not worth waking a machine for. A computer that is stopped or suspended answers 409 with `reason: \"unavailable\"` — the one 409 here that never clears by waiting, because starting the computer is something only you can do. Some others carry `starting` (the guest agent has not answered inside its boot window yet) or `contention` (its guest agent is busy with another call), and both of those are worth sending again. Desktop-session and X-server failures carry no `reason`: the platform cannot distinguish a guest still booting from a logged-out desktop, crashed window manager, or persistently unreachable display, so it does not guess that retrying will help. Switch on `reason`, not on the sentence.\n\n**At most 128 KiB comes out**, and more than that is refused with a 413 rather than truncated — a half a password is not less of an answer, it is a wrong one that looks completely normal. The write cap is different and smaller; see the PUT.\n\nNeeds the `member` role despite being a read, for the reason `GET /files` does: what somebody last copied is not a read of anything this platform models, and a key or a password is the ordinary case rather than the unlucky one.\n\nWindows guests are refused outright — there is no way to run anything in the interactive session there yet.\n\nRequires the `member` role or higher.","operationId":"readClipboard","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"What is on the desktop’s clipboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Clipboard"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"text":"Research notes"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/clipboard' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.clipboard()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.clipboard();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read the desktop clipboard (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headReadClipboard","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"What is on the desktop’s clipboard.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"put":{"summary":"Set the desktop clipboard","description":"Puts text on the desktop’s clipboard, ready to paste. Pair it with `POST /computers/{id}/input` sending `ctrl+v` to get the text into whatever has focus — this call alone leaves it on the clipboard and touches nothing on screen.\n\nUnlike the read, this DRIVES the computer: a suspended one is resumed to serve it, which is a start and is charged like one.\n\n**At most 64 KiB goes in** — half the read cap, and the difference is not taste. The text travels to the guest inside one argument of one command, and Linux caps a single argument at 128 KiB; two layers of base64 stand between your text and that ceiling, so each byte costs about 1.8 of it. Anything larger is a 413.\n\nA NUL byte is refused, because the write is confirmed through a shell and a shell truncates at the first one — so it would land and then be reported as never having landed.\n\n**Empty text CLEARS the clipboard**, and it is the only way to say that. Sending `\"\"` leaves the desktop holding nothing; sending something else is not a clear, it is a different secret in the same place. Omitting `text` altogether is still a 400 — an absent field is a malformed request, an empty one is an instruction.\n\nTwo other 400s here NEVER clear, and they are the ones not to retry. The guest needs `xclip` (X11) or `wl-clipboard` (Wayland) in its image — every golden built since August 2026 has it, so this is a computer created before then, and a computer keeps the image it was created from; install that tool in the guest, which you can do since you have root there, or create a new computer from an image that includes it. And Windows guests are refused outright. Both say which they are, and both carry `reason: \"unsupported\"` — the word for a refusal no action on the computer changes. This matters more on this operation than on the read, because the 409 advice below tells you to retry some failures and neither of these is one of them.\n\nThe write is confirmed by reading the selection back rather than by an exit status, so a 200 here means the desktop is holding your text, not merely that a command ran.\n\n**Not every 409 here is worth retrying, and `reason` in the body is how you tell.** `contention` is the one that clears by itself — \"the desktop did not take the text\" means something else claimed the selection in that instant, a clipboard manager settling, usually — and `starting` means the guest agent has not answered inside its boot window yet, which clears too, just more slowly. Desktop-session and X-server failures are deliberately unclassified because they can also mean nobody is logged in, the window manager crashed, or the display remains unreachable; an absent `reason` gives no retry advice. `unavailable` is the one that does not: the computer is not running, and starting it is the fix rather than another attempt. Branch on that word and never on the sentence, which is prose and is rewritten. A blanket retry on 409 spins until your deadline against a computer that is simply stopped, spending your request allowance a turn at a time and never coming good.\n\nRequires the `member` role or higher.","operationId":"writeClipboard","tags":["Control"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","description":"The text to put on the clipboard, at most 64 KiB. Required; empty clears the clipboard."}},"required":["text"]}}}},"responses":{"200":{"description":"The desktop is holding the text.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PUT \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/clipboard' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"text\":\"https://mandala.computer\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"text\\\":\\\"https://mandala.computer\\\"}\")\n    c = client.computers.get(id)\n    result = c.set_clipboard(body[\"text\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"text\":\"https://mandala.computer\"};\nconst c = await client.computers.get(id);\nconst result = await c.setClipboard(body.text);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdClipboard","tags":["Control"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/agent":{"post":{"summary":"Run an agent loop","description":"One call that drives this computer until the task is done: the loop screenshots, asks a model what to do, performs it, and repeats. Answered by the control plane rather than forwarded to a hypervisor, and it can run for minutes.\n\nThe computer must already be RUNNING. This endpoint will not start one for you: starting is billable, and it is not a decision to make on somebody’s behalf because they sent a prompt. A stopped or suspended computer is refused with status 409, and so is one another agent run is already driving — that one with `reason: \"contention\"`, because it ends when the other run does, as does one whose disk is still being made.\n\nWITH STREAMING, THE DEFAULT, THOSE REFUSALS ARRIVE ON A 200. Every refusal found after the request is accepted — an unknown computer (404), one not running or already driven (409), a credential refused part way through, the model API’s own errors, and on an OpenAI run a batch, `bash` command or screenshot that failed — arrives on the stream as `event: error` with `{request_id, error, status, reason?, cost_usd?, top_up_url?, usage, steps}`, where `status` is the status it would have had and `steps` is the LIST of `AgentStep`s taken before it failed — the list `stream: false` calls `steps_taken`, not the count `AgentResult.steps` carries on `done` (an unexpected failure of the stream itself sends `{request_id, error, status: 500}` alone). Only `stream: false` answers them as HTTP statuses. What is checked before the request is accepted — the key, the role, the rate budget, the body, `X-Model-Key` — is an HTTP status either way.\n\nSend `X-Model-Key` to use your own Anthropic or OpenAI key. The key is never stored; the provider bills you directly and no Mandala AI credits are spent. Select OpenAI with a gpt-*, o-series or computer-use-preview model, or provider: openai. A key whose prefix belongs to the other provider — an sk-ant- key for OpenAI, or any other sk- key for Anthropic — is a 400. OpenAI uses the Responses API with native computer batches and a bash function on this desktop. It sends prompts, screenshots and command output to OpenAI, using stored responses to continue the run. Each OpenAI batch is admitted in full against max_steps and the rate budget before any action runs; a batch that exceeds the remaining allowance is not partially executed. A batch, `bash` command or screenshot that fails ends an OpenAI run as a failure rather than with a `stop`: it carries the status, message and any `reason` the platform answered that call with, and the steps already taken — for a batch, the actions the desktop acknowledged, and the failed one with its `error` when the desktop said which it was. Nothing is replayed, so inspect the desktop before continuing. A command that runs and exits non-zero is not a failure; its output goes back to the model. OpenAI requires your own key. Funded runs are metered by Mandala. Without that header, eligible accounts can use funded AI credits when enabled. Funded runs require a plan, a supported Stripe billing country, and at least $0.25 available for the first model call; otherwise they are refused before streaming (402 with `top_up_url` for insufficient credits; 400, 403 or 503 otherwise, as each of those statuses says). Funded runs use the platform system prompt and tools, default to `claude-sonnet-5-5`, and also allow `claude-opus-5-5`. Custom system prompts and other initially selected models require your own key. Anthropic default fallback routing is enabled and can serve a broader model; each billed attempt uses that model’s rate. Each model call reserves $0.25, then settles actual usage at 1.3× list price (deployment configurable). A completed call can overrun its reserve or max_cost_usd; its full cost is charged before another call is admitted. Usage beyond remaining credits creates debt that survives credit expiry; future grants and top-ups settle it first. If credits become unavailable mid-run it ends with `credits_exhausted`; the cost cap ends with `max_cost`. Solo/Studio/Swarm monthly grants are $5/$10/$50 and expire at billing-period end. $10/$25/$50 top-ups expire 12 months from purchase. Without a plan, existing credits freeze while their original expiry continues; a purchase paid after plan removal is retained for issuance on resubscription if unexpired. Funded prompts, screenshots and tool results are sent to Anthropic under Mandala’s account. See [pricing](https://mandala.computer/pricing), [terms](https://mandala.computer/terms) and [subprocessors](https://mandala.computer/subprocessors).\n\nThe response STREAMS as `text/event-stream` by default, because a run can go a long time between anything worth saying and a buffered answer would be a minute of silence followed by everything at once. Send `stream: false` for a single JSON body at the end instead. On the hosted API a request that is not streaming is cut off at the edge after about 120 seconds (a `524` with no body), and the run’s result — its usage and the steps it took — is lost with it. The run is stopped where it was once the dropped connection reaches the platform, so do not assume it finished. Keep streaming, the default, for anything that may take longer.\n\nA RUN IS KEPT IN A THREAD. Unless you send `store: false`, a run that acted on the desktop, or finished with an answer, is kept as one exchange — the prompt, and the run’s closing text and how it ended (`aborted` for one stopped part way, which may have done more than it reported) — in a thread on this computer, and its `thread_id` comes back on `done` (or the `stream: false` body) and on a failure’s `error`. Send that `thread_id` with the next prompt to continue the conversation: the model is given the earlier turns (the newest 64,000 characters of them, with a note when some are left out) and told they are done, and acts only on the new prompt. A run refused before it acted keeps nothing and starts no new thread; a continuation still names the thread you sent, while it exists. An account keeps 1000 threads: a new one takes the place of the least recently used that this key could delete, and when there is none (a workspace key whose workspace holds none, say) the run is not kept and has no `thread_id` — delete threads you no longer need. Threads are listed and read under `/agent/threads`, and are deleted with their computer.\n\nA CLAUDE RUN CAN ZOOM. Except on claude-opus-4-5 to 4-7 and claude-sonnet-4-6, which use the older computer tool, the loop serves the computer toolset’s `zoom`: a close-up cut from a fresh capture of the screen, of the rectangle the model names in the last full screenshot’s coordinates, so it shows the screen as it is now, which can have changed since that screenshot. A zoom is a step (`action: \"zoom\"`) and counts toward `max_steps`. One that is refused — no full screenshot shown yet, a rectangle outside it, or a capture replaced before it could be cropped — is a step with `error`, and the loop continues. OpenAI runs have no zoom.\n\nEVERY ACTION THE LOOP TAKES SPENDS YOUR RATE BUDGET, the same budget your own calls draw on. An Anthropic step costs what the calls it makes would cost you: an input and the screenshot that shows it, one call for `bash`, a cursor read or a bare screenshot, and two screenshots for a `zoom`. A `key` action with `repeat: n` is one input per press and then the screenshot, n + 1 calls, so a single step can cost up to 101 (`repeat` is at most 100). OpenAI reserves, before each batch runs, the price of one input call for every action in it plus one screenshot for the screenshot after it; its initial screenshot and each `bash` call cost one call each. A run that exhausts it stops where it is and ends with `stop: \"rate_limited\"` rather than failing: the steps already taken are reported, and the work already done to the desktop stands. Treat it the way you treat `max_steps` — as a run that did not finish — and wait rather than raising anything.\n\nA RUN CAN BE REFUSED PART WAY THROUGH. The API key it is running on is re-checked before every model call and before every action or OpenAI batch, so a key deleted — or a membership removed — mid-run stops the run where it is. That arrives as an `error` event on the stream (or the single JSON body when `stream` is false) carrying `reason: \"revoked\"` beside `error`, plus the usage and the steps already taken, which are real and are billed. Most of what this endpoint refuses carries NO word: the model API’s own failures and a stopped or suspended computer arrive without one. A computer another run is already driving, like one whose disk is still being made, carries `reason: \"contention\"`. The run’s opening computer lookup, and an OpenAI run’s batch, `bash` command or screenshot, forward whatever the platform classified its own refusal as, so any of the words above can reach you through this frame. Read it the way you read `reason` anywhere else here, and treat an absent word as no classification.\n\nRequires the `member` role or higher.","operationId":"runAgent","tags":["Agents"],"x-rate-cost":100,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"X-Model-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Your own key for the selected Anthropic or OpenAI provider. Omit for eligible Anthropic-funded runs. A blank header is refused, and so is a key whose prefix belongs to the other provider (sk-ant- for OpenAI, any other sk- key for Anthropic)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","description":"What you want done, in plain language. Required."},"system":{"type":"string","description":"A system prompt for the model. Requires your own X-Model-Key."},"max_cost_usd":{"type":"number","minimum":0.25,"maximum":20,"default":2,"description":"Funded run admission budget in USD. Actual cost can exceed this by the final model call; BYOK ignores it."},"max_steps":{"type":"integer","description":"How many actions the loop may take before it stops. Defaults to 20, and 100 is the most it accepts — a value past that is a 400 rather than a quietly shorter run. A STEP IS ONE ACTION ON THE DESKTOP, not one exchange with the model, and the two do not line up in either direction: one model reply may ask for several actions and spends a step on each, while a reply that asks for none — or a paused turn, resubmitted — costs model tokens and no step at all. Nor does every step take a screenshot; a `bash` call or a cursor read does not, and a `zoom` is a step that takes two. So this bounds the WORK, and bounds your Anthropic spend only loosely. For funded runs, max_cost_usd limits admission but the final call can overrun it. For BYOK, manage spend with your provider. A run that reaches max_steps ends with `stop: \"max_steps\"` and the work already done to the desktop stands. `null` is the same as leaving it out.","minimum":1,"maximum":100,"default":20},"provider":{"type":"string","enum":["anthropic","openai"],"description":"Optional provider. Inferred from claude-*, gpt-*, o-series and computer-use-preview model ids; defaults to Anthropic when neither is supplied. Required for an unknown model namespace. A recognized model/provider mismatch is refused. OpenAI requires X-Model-Key."},"model":{"type":"string","description":"A model id for the selected provider. Funded default: claude-sonnet-5-5; claude-opus-5-5 is also selectable. BYOK supports Anthropic and OpenAI computer-capable models. Defaults: claude-opus-5-5 for Anthropic, gpt-6.1-sol for OpenAI. Funded Anthropic fallback routing can use other models."},"stream":{"type":"boolean","description":"Send `false` for one JSON body at the end instead of an event stream. **This endpoint streams by default**, so omitting it gives you `text/event-stream`. Only the JSON booleans `true` and `false` change the default; any other value, the string `\"false\"` included, is read as if it were left out. On the hosted API a request that is not streaming is cut off at the edge after about 120 seconds (a `524` with no body), and the run’s result — its usage and the steps it took — is lost with it. The run is stopped where it was once the dropped connection reaches the platform, so do not assume it finished. Keep streaming, the default, for anything that may take longer."},"thread_id":{"type":"string","description":"A thread to continue: `thr_` and twenty-four hex characters, from an earlier run’s `thread_id`. Its earlier turns are given to the model as the conversation so far, and this run’s exchange is added to it. A thread of another computer is a `409`, as is a full one (100 turns); one this key cannot see is a `404`."},"store":{"type":"boolean","description":"Whether to keep this run’s exchange in a thread. Default `true`: a run that does something is kept, in the thread you named or a new one, and its `thread_id` comes back on the result. `false` keeps nothing, and cannot be sent with `thread_id`."},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"}},"required":["role","content"]},"description":"The conversation so far, oldest first and without this run’s `prompt`, for a caller that keeps its own rather than a thread here. Given to the model as earlier turns, which it is told are done. Cannot be sent with `thread_id`. A new thread starts with these turns."}},"required":["prompt"]}}}},"responses":{"200":{"description":"An SSE event log of what the loop did, or one JSON body when `stream` is false.\n\nThe stream’s events are `step` (an `AgentStep`, one per action), `text` (`{text}`, what the model said), `done` (the fields below without `steps_taken`: the run finished or stopped) and `error` (see above: the run failed; its `steps` is an `AgentStep` list, not a count). With `stream: false` the answer is the fields below, `steps_taken` included, and a run that fails answers its status with an `AgentFailure` body — `{error, reason?, cost_usd?, top_up_url?, usage, steps_taken}`.\n\nThe stream opens at once with a `: run starting` comment line, before the first model call, and sends `: keepalive` comment lines while the run goes quiet. Lines beginning `:` carry nothing and can be ignored.","content":{"text/event-stream":{"schema":{"type":"string","format":"binary"},"examples":{"streaming":{"summary":"Illustrative native SSE frames for stream:true or omitted stream.","value":": run starting\n\nevent: step\ndata: {\"n\":1,\"tool\":\"bash\",\"detail\":\"firefox 'https://www.google.com/search?q=weather+in+Lisbon' >/dev/null 2>&1 & → exit 0\"}\n\nevent: text\ndata: {\"text\":\"Firefox is open with the search results for weather in Lisbon.\"}\n\nevent: done\ndata: {\"steps\":1,\"stop\":\"end_turn\",\"text\":\"Firefox is open with the search results for weather in Lisbon.\",\"usage\":{\"input_tokens\":1240,\"output_tokens\":95,\"cache_read_tokens\":0,\"cache_write_tokens\":0}}\n\n"},"stream-error":{"summary":"Illustrative error frame after the HTTP stream has opened; work may be incomplete.","value":"event: error\ndata: {\"request_id\":\"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17\",\"error\":\"model API: Anthropic request failed or returned an invalid response.\",\"status\":502,\"usage\":{\"input_tokens\":96,\"output_tokens\":24,\"cache_read_tokens\":64,\"cache_write_tokens\":32},\"steps\":[{\"n\":1,\"tool\":\"computer\",\"action\":\"screenshot\",\"detail\":\"took a screenshot\"}]}\n\n"}}},"application/json":{"schema":{"$ref":"#/components/schemas/AgentResult"},"examples":{"non-streaming":{"summary":"Illustrative result for stream:false.","value":{"steps":1,"stop":"end_turn","text":"Firefox is open with the search results for weather in Lisbon.","usage":{"input_tokens":1240,"output_tokens":95,"cache_read_tokens":0,"cache_write_tokens":0},"steps_taken":[{"n":1,"tool":"bash","detail":"firefox 'https://www.google.com/search?q=weather+in+Lisbon' >/dev/null 2>&1 & → exit 0"}]}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a funded run (no `X-Model-Key`) that this account cannot have: funded AI is off or not available to the account, the body sends `system`, names a model other than claude-sonnet-5-5 or claude-opus-5-5 (an OpenAI model included), or sets `max_cost_usd` outside 0.25 to 20. Send your own key as `X-Model-Key` instead. Answered before streaming. On an OpenAI run, also a `400` the platform answered a batch, `bash` command or screenshot the model asked for, relayed as the run’s failure.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["steps_taken"]}},{"$ref":"#/components/schemas/AgentFailure"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).\n\nOn this route a `401` can also be the model provider’s. With `X-Model-Key`, your key was refused; on a funded run (no `X-Model-Key`) it is the platform’s own provider credential, the message is `Mandala AI is unavailable`, and there is nothing on your side to fix. Only its status is kept: the body is this route’s own error shape, with a sentence of ours rather than the provider’s, and none of the provider’s headers come with it.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["steps_taken"]}},{"$ref":"#/components/schemas/AgentFailure"}]},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Absent on a model provider’s 401.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.\n\nInsufficient funded AI credits before streaming: an `AgentCreditRefusal`, `{error: \"Insufficient AI credits\", top_up_url: \"/billing\"}`, where `top_up_url` is a path on this origin rather than a full URL. If credits can no longer be held when the run makes its first model call, the same refusal and `top_up_url` arrive as the run’s failure instead, an `AgentFailure`. For BYOK, the model API’s own billing refusal is relayed on the run as an AgentFailure. On an OpenAI run, so is a `402` the platform answered a batch, `bash` command or screenshot with: your plan refusing to resume a computer that was suspended during the run, whose message names the limit.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["steps_taken"]},"allOf":[{"not":{"required":["top_up_url"]}}]},{"$ref":"#/components/schemas/AgentCreditRefusal","not":{"required":["steps_taken"]}},{"$ref":"#/components/schemas/AgentFailure"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso a funded run for an account with no billing customer or an unsupported billing country (`A supported billing country is required; use your own key.`). Answered before streaming.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["steps_taken"]}},{"$ref":"#/components/schemas/AgentFailure"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentFailure"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentFailure"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["steps_taken"]}},{"$ref":"#/components/schemas/AgentFailure"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nOn this route a 429 is either your rate budget here (an `Error` body) or the model provider’s rate limit on your `X-Model-Key` — Anthropic’s `rate_limit_error` or OpenAI’s 429 — relayed with its status (an `AgentFailure` body). A funded run relays a 429 from Anthropic on Mandala’s account the same way, with the message `Mandala AI is unavailable`; a spend limit on that account is a 503 instead. The relayed one carries the model API’s own `Retry-After`, in whole seconds, when that API sent one, and may carry none: no wait is invented for it. Streaming, the run’s 429 arrives as a stream event and carries no header.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["steps_taken"]}},{"$ref":"#/components/schemas/AgentFailure"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["steps_taken"]}},{"$ref":"#/components/schemas/AgentFailure"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.\n\nThe model API could not be reached, or the call to it was cut off before it answered. An OpenAI call also ends the run with 502 when OpenAI answers with a 5xx, sends a response that cannot be read or that this platform cannot act on, or takes longer than 120 seconds. On an OpenAI run it can also come from the desktop: a `502` the platform answered a batch, `bash` command or screenshot with — the guest agent not answering, for one — is relayed, and the run answers `502` itself when the desktop returns a screenshot it cannot read or incomplete batch progress, or a call to it fails without an answer. Input is not retried: read `steps_taken` and look at the desktop before continuing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentFailure"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso funded AI being unavailable. Before streaming: `Mandala AI is unavailable`, or `Billing country could not be verified; use your own key or try again.` During a funded run: Mandala’s spend limit with the model provider was reached, or a model call could not be priced or recorded. The run ends with `Mandala AI is unavailable`, and the calls already made stay charged.\n\nAlso `reason: \"draining\"`, with a `Retry-After` header: the platform is about to restart for an update and is not starting new agent runs for a few minutes. Runs already going are not affected. Nothing was started, held, charged or recorded for this request, so send the same one again after `Retry-After` seconds. Answered before anything streams, as an `Error` body.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error","not":{"required":["steps_taken"]}},{"$ref":"#/components/schemas/AgentFailure"}]}}},"headers":{"Retry-After":{"description":"Present on the `reason: \"draining\"` refusal: seconds to wait before sending the same request again.","schema":{"type":"integer"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.\n\nOn this route, Anthropic’s own `timeout_error` from the model API, relayed with its status: the model call took too long, and the run stopped there. An OpenAI call that times out is a 502 instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentFailure"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"529":{"description":"The model API is overloaded (`overloaded_error`), relayed with its status intact. Not a status this platform sends of its own; the run stopped there, and a retry later can succeed. With your own Anthropic key, every status the model API answers reaches you as itself, with a fixed message naming the provider and status, in the `AgentFailure` body. OpenAI’s 4xx statuses reach you as themselves with a fixed message naming the status, and its 5xx as 502. A funded run relays Anthropic’s status with the message `Mandala AI is unavailable`, except that a spend limit on Mandala’s account is a 503.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentFailure"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/agent' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"X-Model-Key: ${X_MODEL_KEY:?Set X_MODEL_KEY}\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"prompt\":\"Open Firefox and search for the weather in Lisbon\",\"stream\":false}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"prompt\\\":\\\"Open Firefox and search for the weather in Lisbon\\\",\\\"stream\\\":false}\")\n    c = client.computers.get(id)\n    result = c.agent_once(body[\"prompt\"], model_key=os.environ[\"X_MODEL_KEY\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst modelKey = process.env.X_MODEL_KEY;\nif (!modelKey) throw new Error(\"Set X_MODEL_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"prompt\":\"Open Firefox and search for the weather in Lisbon\",\"stream\":false};\nconst c = await client.computers.get(id);\nconst result = await c.agentOnce({prompt: body.prompt, modelKey});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdAgent","tags":["Agents"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/chat/completions":{"post":{"summary":"Drive a computer through an OpenAI-shaped endpoint","description":"The agent loop behind a door an OpenAI client already knows how to open, for callers who would rather point an existing client at a base URL than integrate a new one. Same loop and the same `X-Model-Key` behavior as the BYOK computer agent. This route always requires your own key; funded AI is unavailable here.\n\nFour things are ours rather than OpenAI’s. `computer_id` on the body is required, and there is nowhere else to put it: this endpoint has no computer in its path. `provider` and `max_steps` are optional and mean what they mean on `/computers/{id}/agent`. And the response carries an `agent` block beside the OpenAI fields, saying what the loop did. Everything else reads the way an OpenAI client already writes it — the prompt is the last `user` message, the `user` and `assistant` messages before it are the conversation so far (given to the model as turns it is told are done), and any `system` messages become the system prompt. `thread_id` and `store` keep and continue a conversation as on `/computers/{id}/agent`; with `thread_id` the thread’s own turns are the conversation and earlier messages you send are not read, so resending the whole history each call is harmless. A new thread starts with the messages you sent, and its id comes back as `agent.thread_id` (on the last chunk when streaming). Only the documented top-level fields are accepted; unknown fields are a `400`.\n\nBESIDES THE KEY IT NEEDS, ONE BEHAVIOUR DIFFERS FROM THE ROUTE ABOVE, because this door follows OpenAI’s contract rather than ours: **this endpoint does not stream unless you ask it to** — omit `stream` and you get one JSON completion, which is the opposite of `/computers/{id}/agent`. `model` and `provider` choose the provider the same way on both routes; use a model that supports computer use, such as gpt-6.1-sol.\n\nOn the hosted API a run that does not stream — this endpoint’s default — is cut off at the edge after about 120 seconds (a `524` with no body), and its result, the usage and the steps it took, is lost with it. The run is stopped where it was once the dropped connection reaches the platform, so do not assume it finished. A run at the default `max_steps` can easily take longer: send `stream: true` for anything that may.\n\nThe computer must already be running — this endpoint will not start one for you, because starting is billable and not a decision to make on your behalf.\n\nRequires the `member` role or higher.","operationId":"createChatCompletion","tags":["Agents"],"x-rate-cost":100,"parameters":[{"name":"X-Model-Key","in":"header","required":true,"schema":{"type":"string"},"description":"Your own key for the selected Anthropic or OpenAI provider. Never stored. The provider bills you directly; no Mandala AI credits are spent. A key whose prefix belongs to the other provider (sk-ant- for OpenAI, any other sk- key for Anthropic) is a 400."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"computer_id":{"type":"string","description":"Which of your computers to drive. Required."},"messages":{"type":"array","items":{"type":"object"},"description":"OpenAI-shaped messages. The last `user` message is the prompt. Required."},"provider":{"type":"string","enum":["anthropic","openai"],"description":"Optional provider. Inferred from claude-*, gpt-*, o-series and computer-use-preview model ids; defaults to Anthropic when neither is supplied. Required for an unknown model namespace. A recognized model/provider mismatch is refused. OpenAI requires X-Model-Key."},"model":{"type":"string","description":"A computer-capable Anthropic or OpenAI model id, passed to the selected provider unchanged. Defaults: claude-opus-5-5 for Anthropic, gpt-6.1-sol for OpenAI."},"max_steps":{"type":"integer","description":"How many actions the loop may take before it stops, as on `POST /computers/{id}/agent` — defaults to 20, and 100 is the most it accepts. A value past that is a 400. Not an OpenAI field; it is ours, and an OpenAI client that does not send it gets the default, as does `null`.","minimum":1,"maximum":100,"default":20},"stream":{"type":"boolean","description":"Send `true` for an event stream. **This endpoint does not stream by default**, so omitting it gives you one JSON body — the opposite of `POST /computers/{id}/agent`. Only the JSON booleans `true` and `false` change the default; any other value, the string `\"false\"` included, is read as if it were left out. On the hosted API a run that does not stream — this endpoint’s default — is cut off at the edge after about 120 seconds (a `524` with no body), and its result, the usage and the steps it took, is lost with it. The run is stopped where it was once the dropped connection reaches the platform, so do not assume it finished. A run at the default `max_steps` can easily take longer: send `stream: true` for anything that may."},"thread_id":{"type":"string","description":"A thread to continue: `thr_` and twenty-four hex characters, from an earlier run’s `thread_id`. Its earlier turns are given to the model as the conversation so far, and this run’s exchange is added to it. A thread of another computer is a `409`, as is a full one (100 turns); one this key cannot see is a `404`."},"store":{"type":"boolean","description":"Whether to keep this run’s exchange in a thread. Default `true`: a run that does something is kept, in the thread you named or a new one, and its `thread_id` comes back on the result. `false` keeps nothing, and cannot be sent with `thread_id`."}},"required":["computer_id","messages"],"additionalProperties":false}}}},"responses":{"200":{"description":"One `ChatCompletion`, or with `stream: true` an SSE stream of OpenAI-style chunks.\n\nEach chunk is `data: {id, object: \"chat.completion.chunk\", created, model: \"computer-use-agent\", choices: [{index: 0, delta, finish_reason}]}`. The first chunk’s `delta` carries `role: \"assistant\"`; each action the loop takes arrives as a line of `delta.content` (`[n] what it did`), then the model’s text; the last chunk has an empty `delta` and the `finish_reason` described on `ChatCompletionChoice`. The stream carries no `usage` and no `agent` block, so a streamed run cannot tell a finished run from one that ran out of rate budget — send `stream: false` when that matters and the run is short enough. A run that fails arrives as `data: {error, request_id}` with the `OpenAIErrorDetail` shape under `error`. The stream ends with `data: [DONE]`, and lines starting `:` carry nothing: a `: run starting` line first, then `: keepalive` lines while the run goes quiet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCompletion"},"examples":{"non-streaming":{"summary":"Illustrative successful response.","value":{"id":"agentrun-vm-a1b2c3d4e5f6-a1b2c3d4e5f6","object":"chat.completion","created":1789560000,"model":"computer-use-agent","choices":[{"index":0,"message":{"role":"assistant","content":"Firefox is open with the search results for weather in Lisbon."},"finish_reason":"stop"}],"usage":{"prompt_tokens":1240,"completion_tokens":95,"total_tokens":1335},"agent":{"computer_id":"vm-a1b2c3d4e5f6","steps":1,"stop":"end_turn"}}}}},"text/event-stream":{"schema":{"type":"string","format":"binary"},"examples":{"streaming":{"summary":"Illustrative OpenAI-shaped SSE chunks for stream:true.","value":": run starting\n\ndata: {\"id\":\"agentrun-vm-a1b2c3d4e5f6-a1b2c3d4e5f6\",\"object\":\"chat.completion.chunk\",\"created\":1789560000,\"model\":\"computer-use-agent\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"[1] firefox 'https://www.google.com/search?q=weather+in+Lisbon' >/dev/null 2>&1 & → exit 0\\n\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"agentrun-vm-a1b2c3d4e5f6-a1b2c3d4e5f6\",\"object\":\"chat.completion.chunk\",\"created\":1789560000,\"model\":\"computer-use-agent\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Firefox is open with the search results for weather in Lisbon.\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"agentrun-vm-a1b2c3d4e5f6-a1b2c3d4e5f6\",\"object\":\"chat.completion.chunk\",\"created\":1789560000,\"model\":\"computer-use-agent\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n"},"stream-error":{"summary":"Illustrative error frame after the HTTP stream has opened; work may be incomplete.","value":"data: {\"error\":{\"message\":\"model API: Anthropic request failed or returned an invalid response.\",\"code\":502,\"usage\":{\"prompt_tokens\":96,\"completion_tokens\":24,\"total_tokens\":120},\"steps\":[{\"n\":1,\"tool\":\"computer\",\"action\":\"screenshot\",\"detail\":\"took a screenshot\"}],\"agent\":{\"computer_id\":\"vm-a1b2c3d4e5f6\",\"usage\":{\"input_tokens\":96,\"output_tokens\":24,\"cache_read_tokens\":64,\"cache_write_tokens\":32},\"steps\":[{\"n\":1,\"tool\":\"computer\",\"action\":\"screenshot\",\"detail\":\"took a screenshot\"}]}},\"request_id\":\"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17\"}\n\ndata: [DONE]\n\n"}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/OpenAIError"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).\n\nOn this route a `401` can also be the model provider’s. With `X-Model-Key`, your key was refused; on a funded run (no `X-Model-Key`) it is the platform’s own provider credential, the message is `Mandala AI is unavailable`, and there is nothing on your side to fix. Only its status is kept: the body is this route’s own error shape, with a sentence of ours rather than the provider’s, and none of the provider’s headers come with it.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/OpenAIError"}]},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Absent on a model provider’s 401.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.\n\nOn this route, the model API’s own refusal relayed as it came (`billing_error`: the account behind your `X-Model-Key` has a billing problem). On an OpenAI run it can also be your plan here refusing to resume a computer that was suspended during the run, relayed the same way; its message names the limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIError"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/OpenAIError"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIError"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIError"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/OpenAIError"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nOn this route a 429 is either your rate budget here (an `Error` body) or the model provider’s rate limit on your `X-Model-Key` — Anthropic’s `rate_limit_error` or OpenAI’s 429 — relayed with its status (an `OpenAIError` body). The relayed one carries the model API’s own `Retry-After`, in whole seconds, when that API sent one, and may carry none: no wait is invented for it. Streaming, the run’s 429 arrives as a stream event and carries no header.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/OpenAIError"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/OpenAIError"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.\n\nThe model API could not be reached, or the call to it was cut off before it answered. An OpenAI call also ends the run with 502 when OpenAI answers with a 5xx, sends a response that cannot be read or that this platform cannot act on, or takes longer than 120 seconds. On an OpenAI run it can also come from the desktop: a `502` the platform answered a batch, `bash` command or screenshot with — the guest agent not answering, for one — is relayed, and the run answers `502` itself when the desktop returns a screenshot it cannot read or incomplete batch progress, or a call to it fails without an answer. Input is not retried: read `steps` and look at the desktop before continuing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIError"},"examples":{"run-error":{"summary":"A run failure uses the nested OpenAI envelope. Streaming failures can arrive after HTTP 200.","value":{"error":{"message":"model API: Anthropic request failed or returned an invalid response.","code":502,"usage":{"prompt_tokens":96,"completion_tokens":24,"total_tokens":120},"steps":[{"n":1,"tool":"computer","action":"screenshot","detail":"took a screenshot"}],"agent":{"computer_id":"vm-a1b2c3d4e5f6","usage":{"input_tokens":96,"output_tokens":24,"cache_read_tokens":64,"cache_write_tokens":32},"steps":[{"n":1,"tool":"computer","action":"screenshot","detail":"took a screenshot"}]}},"request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nA failure of the run, in the `OpenAIError` envelope: the host holding the computer could not be reached.\n\nAlso `reason: \"draining\"`, with a `Retry-After` header: the platform is about to restart for an update and is not starting new agent runs for a few minutes. Runs already going are not affected. Nothing was started, held, charged or recorded for this request, so send the same one again after `Retry-After` seconds. Answered before anything streams, as an `Error` body.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/OpenAIError"}]}}},"headers":{"Retry-After":{"description":"Present on the `reason: \"draining\"` refusal: seconds to wait before sending the same request again.","schema":{"type":"integer"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"504":{"description":"The operation did not finish before its deadline — a window action the guest accepted but did not report on, or an artifact read or delete. It may already have happened, so this is an uncertain outcome rather than permission to repeat it: check before sending it again.\n\nOn this route, Anthropic’s own `timeout_error` from the model API, relayed with its status: the model call took too long, and the run stopped there. An OpenAI call that times out is a 502 instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIError"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"529":{"description":"The model API is overloaded (`overloaded_error`), relayed with its status intact. Not a status this platform sends of its own; the run stopped there, and a retry later can succeed. With an Anthropic key, every status the model API answers reaches you as itself, with a fixed message naming the provider and status, in the `OpenAIError` body. OpenAI’s 4xx statuses reach you as themselves with a fixed message naming the status, and its 5xx as 502.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIError"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/chat/completions' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"X-Model-Key: ${X_MODEL_KEY:?Set X_MODEL_KEY}\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"computer_id\":\"vm-a1b2c3d4e5f6\",\"messages\":[{\"role\":\"user\",\"content\":\"Open Firefox and search for the weather in Lisbon\"}]}'"},{"lang":"Python","label":"Python — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.17.0.\n# Python 3.10+; standard library only.\nimport json, os\nfrom urllib.request import Request, urlopen\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"chat\" + \"/\" + \"completions\"\nmodel_key = os.environ.get(\"X_MODEL_KEY\")\nif not model_key:\n    raise ValueError(\"Set X_MODEL_KEY\")\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"], \"X-Model-Key\": model_key, \"Content-Type\": \"application/json\"}\nbody = json.loads(\"{\\\"computer_id\\\":\\\"vm-a1b2c3d4e5f6\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"Open Firefox and search for the weather in Lisbon\\\"}]}\")\nrequest = Request(url, headers=headers, method=\"POST\", data=json.dumps(body).encode(\"utf-8\"))\nwith urlopen(request) as response:\n    print(json.load(response))"},{"lang":"TypeScript","label":"TypeScript — direct HTTP (no released wrapper)","source":"// Direct HTTP: this operation has no wrapper in mandala-computer@0.17.0.\n// Node.js 22+; no package required.\n\nconst baseUrl = (process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\").replace(/\\/+$/, \"\");\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst url = new URL(baseUrl + \"/\" + \"chat\" + \"/\" + \"completions\");\nconst modelKey = process.env.X_MODEL_KEY;\nif (!modelKey) throw new Error(\"Set X_MODEL_KEY\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey, \"X-Model-Key\": modelKey, \"Content-Type\": \"application/json\"};\nconst body = {\"computer_id\":\"vm-a1b2c3d4e5f6\",\"messages\":[{\"role\":\"user\",\"content\":\"Open Firefox and search for the weather in Lisbon\"}]};\nconst response = await fetch(url, {method: \"POST\", headers, body: JSON.stringify(body)});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsChatCompletions","tags":["Agents"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/agent/threads":{"get":{"summary":"List agent threads","description":"The conversations agent runs have kept, most recently used first: from `POST /computers/{id}/agent` and `POST /chat/completions`, unless a run sent `store: false`. A key confined to a workspace lists that workspace’s. An account keeps its 1000 most recently used threads; making another deletes the one used longest ago. Members and owners may read them; viewers cannot, since a prompt is where private text ends up.\n\nRequires the `member` role or higher.","operationId":"listAgentThreads","tags":["Agents"],"x-rate-cost":1,"parameters":[{"name":"computer_id","in":"query","required":false,"schema":{"type":"string"},"description":"Only this computer’s threads."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"How many to return. Outside 1 to 100 is a `400`."},{"name":"before","in":"query","required":false,"schema":{"type":"string"},"description":"The id of the last thread of the previous page, for the page after it. One this key cannot see is a `400`."}],"responses":{"200":{"description":"One page of threads.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentThreadList"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"threads":[{"id":"thr_0123456789abcdef01234567","computer_id":"vm-a1b2c3d4e5f6","title":"Open Firefox and search for the weather in Lisbon","turns":4,"created_at":"2026-10-18T09:00:00.000Z","updated_at":"2026-10-18T09:04:00.000Z"}],"has_more":false}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/agent/threads' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.agent_threads.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.agentThreads.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List agent threads (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headListAgentThreads","tags":["Agents"],"parameters":[{"name":"computer_id","in":"query","required":false,"schema":{"type":"string"},"description":"Only this computer’s threads."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"How many to return. Outside 1 to 100 is a `400`."},{"name":"before","in":"query","required":false,"schema":{"type":"string"},"description":"The id of the last thread of the previous page, for the page after it. One this key cannot see is a `400`."}],"responses":{"200":{"description":"One page of threads.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsAgentThreads","tags":["Agents"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/agent/threads/{thread}":{"get":{"summary":"Read an agent thread","description":"One thread and every turn in it, oldest first: each `user` turn is a prompt, and each `assistant` turn is how the run answered it — its closing text, `stop`, `steps`, and `cost_usd` on a funded run. A thread this key cannot see is a `404`.\n\nRequires the `member` role or higher.","operationId":"getAgentThread","tags":["Agents"],"x-rate-cost":1,"parameters":[{"name":"thread","in":"path","required":true,"schema":{"type":"string"},"description":"A `thread_id` from an agent run or `GET /agent/threads` (`thr_` followed by 24 lowercase hex characters)."}],"responses":{"200":{"description":"The thread.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentThread"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"thr_0123456789abcdef01234567","computer_id":"vm-a1b2c3d4e5f6","title":"Open Firefox and search for the weather in Lisbon","turns":4,"created_at":"2026-10-18T09:00:00.000Z","updated_at":"2026-10-18T09:04:00.000Z","messages":[{"role":"user","text":"Open Firefox and search for the weather in Lisbon","created_at":"2026-10-18T09:00:00.000Z"},{"role":"assistant","text":"Lisbon is 21 °C and sunny.","stop":"end_turn","steps":6,"created_at":"2026-10-18T09:00:00.000Z"},{"role":"user","text":"Now do the same for Porto","created_at":"2026-10-18T09:04:00.000Z"},{"role":"assistant","text":"Porto is 18 °C with light rain.","stop":"end_turn","steps":4,"created_at":"2026-10-18T09:04:00.000Z"}]}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/agent/threads/thr_0123456789abcdef01234567' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    thread = \"thr_0123456789abcdef01234567\"\n    result = client.agent_threads.get(thread)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst thread = \"thr_0123456789abcdef01234567\";\nconst result = await client.agentThreads.get(thread);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read an agent thread (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetAgentThread","tags":["Agents"],"parameters":[{"name":"thread","in":"path","required":true,"schema":{"type":"string"},"description":"A `thread_id` from an agent run or `GET /agent/threads` (`thr_` followed by 24 lowercase hex characters)."}],"responses":{"200":{"description":"The thread.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"delete":{"summary":"Delete an agent thread","description":"The thread and its turns are gone, and its id can no longer be continued. What the runs did to the desktop stands. A thread this key cannot see is a `404`.\n\nRequires the `member` role or higher.","operationId":"deleteAgentThread","tags":["Agents"],"x-rate-cost":1,"parameters":[{"name":"thread","in":"path","required":true,"schema":{"type":"string"},"description":"A `thread_id` from an agent run or `GET /agent/threads` (`thr_` followed by 24 lowercase hex characters)."}],"responses":{"200":{"description":"Gone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/agent/threads/thr_0123456789abcdef01234567' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    thread = \"thr_0123456789abcdef01234567\"\n    result = client.agent_threads.delete(thread)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst thread = \"thr_0123456789abcdef01234567\";\nconst result = await client.agentThreads.delete(thread);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsAgentThreadsByThread","tags":["Agents"],"security":[],"parameters":[{"name":"thread","in":"path","required":true,"schema":{"type":"string"},"description":"A `thread_id` from an agent run or `GET /agent/threads` (`thr_` followed by 24 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/agent/runs":{"post":{"summary":"Start an agent run in the background","description":"Give the computer a job and walk away: the same loop as `POST /computers/{id}/agent`, with the same body except `stream` (refused) and `store: false` (refused: a background run is kept so it can be read back), started and then read back by id instead of streamed. The run goes on when this request ends, and is not cut at the edge after 120 seconds the way a non-streaming agent call is.\n\nThis answers `202` once the run holds the computer, with the run as it stands (`status: \"running\"`). What refuses a run before that — an unknown or just-deleted computer (`404`), one not running (`409`), one already driven or whose workspace is changing (`409` with `reason: \"contention\"`), a credential refused, the funded credit hold — is answered here as its own status with `{error, reason?, top_up_url?}`, and no run is recorded. Everything after it, the model API’s errors included, ends the run as `failed` with that `error`.\n\nPoll `GET /agent/runs/{run}` for its steps, cost and ending, or cancel it with `POST /agent/runs/{run}/cancel`. A run keeps its thread as the agent route does, and `thread_id` on the run says which. `X-Model-Key` is held in memory for the run’s life and never stored. The run uses the API key that started it: deleting that key stops it at its next step, as on the agent route. A run the control plane was executing when it restarted is marked `interrupted`, with the steps it had taken; it is not resumed. Inspect the desktop before starting another.\n\nThe screenshot each step returns, the one the model is shown next, is kept with the run for seven days (within per-run and per-account size limits) so the run can be replayed step by step on the computer’s Activities tab in the dashboard. Those screenshots show whatever was on the screen, so treat them as being as sensitive as the desktop. They are deleted with the run, and with its computer.\n\nRequires the `member` role or higher.","operationId":"createAgentRun","tags":["Agents"],"x-rate-cost":100,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"X-Model-Key","in":"header","required":false,"schema":{"type":"string"},"description":"As on `POST /computers/{id}/agent`. Held in memory while the run goes, and never stored."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","description":"What you want done, in plain language. Required."},"system":{"type":"string","description":"A system prompt for the model. Requires your own X-Model-Key."},"max_cost_usd":{"type":"number","minimum":0.25,"maximum":20,"default":2,"description":"As on `POST /computers/{id}/agent`."},"max_steps":{"type":"integer","description":"As on `POST /computers/{id}/agent`: defaults to 20, at most 100.","minimum":1,"maximum":100,"default":20},"provider":{"type":"string","enum":["anthropic","openai"],"description":"As on `POST /computers/{id}/agent`."},"model":{"type":"string","description":"As on `POST /computers/{id}/agent`."},"thread_id":{"type":"string","description":"A thread to continue, as on `POST /computers/{id}/agent`."},"store":{"type":"boolean","description":"As on `POST /computers/{id}/agent`. Default `true`."},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string"}},"required":["role","content"]},"description":"As on `POST /computers/{id}/agent`."}},"required":["prompt"]}}}},"responses":{"202":{"description":"The run, running.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRun"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"run_89abcdef0123456789abcdef","computer_id":"vm-a1b2c3d4e5f6","thread_id":"thr_0123456789abcdef01234567","status":"running","prompt":"Download this month’s invoices into ~/invoices","model":"claude-sonnet-5-5","max_steps":60,"steps":0,"steps_taken":[],"created_at":"2026-10-18T09:00:00.000Z","updated_at":"2026-10-18T09:00:00.000Z","ended_at":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso `reason: \"draining\"`, with a `Retry-After` header: the platform is about to restart for an update and is not starting new agent runs for a few minutes. Runs already going are not affected. Nothing was started, held, charged or recorded for this request, so send the same one again after `Retry-After` seconds. Answered before anything streams, as an `Error` body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Present on the `reason: \"draining\"` refusal: seconds to wait before sending the same request again.","schema":{"type":"integer"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/agent/runs' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"X-Model-Key: ${X_MODEL_KEY:?Set X_MODEL_KEY}\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"prompt\":\"Download this month’s invoices into ~/invoices\",\"max_steps\":60}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"prompt\\\":\\\"Download this month’s invoices into ~/invoices\\\",\\\"max_steps\\\":60}\")\n    result = client.agent_runs.start(id, body[\"prompt\"], max_steps=body[\"max_steps\"], model_key=os.environ[\"X_MODEL_KEY\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst modelKey = process.env.X_MODEL_KEY;\nif (!modelKey) throw new Error(\"Set X_MODEL_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"prompt\":\"Download this month’s invoices into ~/invoices\",\"max_steps\":60};\nconst result = await client.agentRuns.start(id, {prompt: body.prompt, maxSteps: body.max_steps, modelKey});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdAgentRuns","tags":["Agents"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/agent/runs":{"get":{"summary":"List agent runs","description":"Background runs, newest first. A key confined to a workspace lists that workspace’s. Each run’s `steps_taken` is empty here; read the run for them. An account keeps its 1000 newest runs that have ended, and every run still going. Members and owners may read them; viewers cannot.\n\nRequires the `member` role or higher.","operationId":"listAgentRuns","tags":["Agents"],"x-rate-cost":1,"parameters":[{"name":"computer_id","in":"query","required":false,"schema":{"type":"string"},"description":"Only this computer’s runs."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["running","completed","failed","cancelled","interrupted"]},"description":"Only runs in this status. Anything else is a `400`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"How many to return. Outside 1 to 100 is a `400`."},{"name":"before","in":"query","required":false,"schema":{"type":"string"},"description":"The id of the last run of the previous page. One this key cannot see is a `400`."}],"responses":{"200":{"description":"One page of runs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRunList"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"runs":[{"id":"run_89abcdef0123456789abcdef","computer_id":"vm-a1b2c3d4e5f6","thread_id":"thr_0123456789abcdef01234567","status":"completed","prompt":"Download this month’s invoices into ~/invoices","model":"claude-sonnet-5-5","max_steps":60,"steps":2,"cost_usd":0.42,"result":{"steps":2,"cost_usd":0.42,"stop":"end_turn","text":"Saved 3 invoices to ~/invoices.","usage":{"input_tokens":41200,"output_tokens":820,"cache_read_tokens":36000,"cache_write_tokens":3100}},"steps_taken":[],"created_at":"2026-10-18T09:00:00.000Z","updated_at":"2026-10-18T09:03:00.000Z","ended_at":"2026-10-18T09:03:00.000Z"}],"has_more":false}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/agent/runs' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.agent_runs.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.agentRuns.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List agent runs (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headListAgentRuns","tags":["Agents"],"parameters":[{"name":"computer_id","in":"query","required":false,"schema":{"type":"string"},"description":"Only this computer’s runs."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["running","completed","failed","cancelled","interrupted"]},"description":"Only runs in this status. Anything else is a `400`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"How many to return. Outside 1 to 100 is a `400`."},{"name":"before","in":"query","required":false,"schema":{"type":"string"},"description":"The id of the last run of the previous page. One this key cannot see is a `400`."}],"responses":{"200":{"description":"One page of runs.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsAgentRuns","tags":["Agents"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/agent/runs/{run}":{"get":{"summary":"Read an agent run","description":"The run as it stands: its status, every step so far, its cost on a funded run, and once it has ended, its `result` (`completed`) or `error` (`failed`, `interrupted`). Poll it; a few seconds apart is plenty, since a step takes at least that long. A run this key cannot see is a `404`.\n\nRequires the `member` role or higher.","operationId":"getAgentRun","tags":["Agents"],"x-rate-cost":1,"parameters":[{"name":"run","in":"path","required":true,"schema":{"type":"string"},"description":"A run id from `POST /computers/{id}/agent/runs` (`run_` followed by 24 lowercase hex characters)."}],"responses":{"200":{"description":"The run.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRun"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"run_89abcdef0123456789abcdef","computer_id":"vm-a1b2c3d4e5f6","thread_id":"thr_0123456789abcdef01234567","status":"completed","prompt":"Download this month’s invoices into ~/invoices","model":"claude-sonnet-5-5","max_steps":60,"steps":2,"cost_usd":0.42,"result":{"steps":2,"cost_usd":0.42,"stop":"end_turn","text":"Saved 3 invoices to ~/invoices.","usage":{"input_tokens":41200,"output_tokens":820,"cache_read_tokens":36000,"cache_write_tokens":3100}},"steps_taken":[{"n":1,"cost_usd":0.21,"tool":"computer","action":"left_click","detail":"clicked (512, 300)"},{"n":2,"cost_usd":0.42,"tool":"bash","detail":"ls ~/invoices"}],"created_at":"2026-10-18T09:00:00.000Z","updated_at":"2026-10-18T09:03:00.000Z","ended_at":"2026-10-18T09:03:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/agent/runs/run_89abcdef0123456789abcdef' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    run = \"run_89abcdef0123456789abcdef\"\n    result = client.agent_runs.get(run)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst run = \"run_89abcdef0123456789abcdef\";\nconst result = await client.agentRuns.get(run);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read an agent run (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetAgentRun","tags":["Agents"],"parameters":[{"name":"run","in":"path","required":true,"schema":{"type":"string"},"description":"A run id from `POST /computers/{id}/agent/runs` (`run_` followed by 24 lowercase hex characters)."}],"responses":{"200":{"description":"The run.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsAgentRunsByRun","tags":["Agents"],"security":[],"parameters":[{"name":"run","in":"path","required":true,"schema":{"type":"string"},"description":"A run id from `POST /computers/{id}/agent/runs` (`run_` followed by 24 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/agent/runs/{run}/cancel":{"post":{"summary":"Cancel an agent run","description":"Stops a running run where it is: the model call in flight is abandoned and no further action is taken. What it already did to the desktop stands, and so does what it cost. The run answers as `cancelled`. Cancelling a run that already ended answers it as it is, unchanged. A run this key cannot see is a `404`.\n\nRequires the `member` role or higher.","operationId":"cancelAgentRun","tags":["Agents"],"x-rate-cost":1,"parameters":[{"name":"run","in":"path","required":true,"schema":{"type":"string"},"description":"A run id from `POST /computers/{id}/agent/runs` (`run_` followed by 24 lowercase hex characters)."}],"responses":{"200":{"description":"The run.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRun"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"run_89abcdef0123456789abcdef","computer_id":"vm-a1b2c3d4e5f6","thread_id":"thr_0123456789abcdef01234567","status":"completed","prompt":"Download this month’s invoices into ~/invoices","model":"claude-sonnet-5-5","max_steps":60,"steps":2,"cost_usd":0.42,"result":{"steps":2,"cost_usd":0.42,"stop":"end_turn","text":"Saved 3 invoices to ~/invoices.","usage":{"input_tokens":41200,"output_tokens":820,"cache_read_tokens":36000,"cache_write_tokens":3100}},"steps_taken":[{"n":1,"cost_usd":0.21,"tool":"computer","action":"left_click","detail":"clicked (512, 300)"},{"n":2,"cost_usd":0.42,"tool":"bash","detail":"ls ~/invoices"}],"created_at":"2026-10-18T09:00:00.000Z","updated_at":"2026-10-18T09:03:00.000Z","ended_at":"2026-10-18T09:03:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/agent/runs/run_89abcdef0123456789abcdef/cancel' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    run = \"run_89abcdef0123456789abcdef\"\n    result = client.agent_runs.cancel(run)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst run = \"run_89abcdef0123456789abcdef\";\nconst result = await client.agentRuns.cancel(run);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsAgentRunsByRunCancel","tags":["Agents"],"security":[],"parameters":[{"name":"run","in":"path","required":true,"schema":{"type":"string"},"description":"A run id from `POST /computers/{id}/agent/runs` (`run_` followed by 24 lowercase hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/files":{"get":{"summary":"Download a file","description":"Reads a file out of the guest. The response body is the file’s bytes.\n\nNeeds the `member` role despite being a read. `path` is an arbitrary read of the guest’s filesystem rather than a read of anything this platform models — `/home/user/.ssh/id_rsa` is one call — which is strictly more than a viewer is given anywhere else.\n\n**A whole file is capped at 64 MiB, and a `Range` is how you get past that.** Without one this endpoint refuses anything larger with a 413, because the bytes cross the guest agent in chunks and one request holds that channel for as long as it takes. With one, the cap applies to the WINDOW you asked for rather than to the file, so a 2 GB build output is something you page through rather than something you cannot fetch.\n\nA satisfied range answers `206` with a `Content-Range` giving the bytes you got and the file’s total length. **You may get fewer bytes than you asked for** — a window past 64 MiB is trimmed to it rather than refused, since you cannot know the limit before you ask — so read the `Content-Range` and ask again from where it ended. That is the paging loop, and it needs nothing else:\n\n```\ncurl -H \"Authorization: Bearer $MANDALA_KEY\" -H \"Range: bytes=0-1048575\" \\\n  \".../files?path=/home/user/out.tar\" -D - -o part-0\n  -> 206, Content-Range: bytes 0-1048575/2147483648\n```\n\nWhich end is trimmed follows the end you anchored: `bytes=N-` keeps its start and loses its far end, while `bytes=-N` keeps its END — an over-long tail is still the tail of the file, never the middle of it.\n\nA range that names no byte the file has is a `416` whose `Content-Range` carries the real length. A file whose length the guest cannot report — a `/proc` entry, say — answers `Accept-Ranges: none` and ignores your `Range`, sending the whole thing with a `200`; the status is how you tell. And the `413` you get for asking for a whole file that is too big names this header in its message, since that refusal is exactly when you need to know the option exists.\n\nRequires the `member` role or higher.","operationId":"downloadFile","tags":["Files"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Absolute path inside the guest."},{"name":"no_wake","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Set to 1 to require a running computer without automatic resume: a computer that is not running answers `409` with `reason: unavailable` rather than being resumed. Any other value is a `400`. The dashboard always sends this."},{"name":"Range","in":"header","required":false,"schema":{"type":"string"},"description":"A single byte range — `bytes=0-1048575`, `bytes=1048576-` or `bytes=-4096` for the tail. Only the first range of a multi-range request is served. A `bytes=` spec that cannot be read is a 400; a unit that is not `bytes` is ignored and you get the whole file."}],"responses":{"200":{"description":"The file, or the window you asked for.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"},"examples":{"success":{"summary":"Raw UTF-8 file bytes, including the final LF.","value":"Hello from Mandala.\n"}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"206":{"description":"The file, or the window you asked for.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"},"examples":{"byte-range":{"summary":"Raw bytes for request Range: bytes=0-4.","value":"Hello"}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"416":{"description":"Your `Range` named no byte the file has. The `Content-Range` on the refusal carries the file’s actual length, so you can ask again without guessing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/files?path=%2Fhome%2Fuser%2Fnotes.txt' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -o download.bin"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    query = {\"path\":\"/home/user/notes.txt\"}\n    c = client.computers.get(id)\n    result = c.read_file(query[\"path\"])\n    Path(\"download.bin\").write_bytes(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { writeFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst query = {\"path\":\"/home/user/notes.txt\"};\nconst c = await client.computers.get(id);\nconst result = await c.readFile(query.path);\nawait writeFile(\"download.bin\", result);"}]},"head":{"summary":"Download a file (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. This one checks metadata instead of reading the content: it never resumes a guest, consumes output or opens a stream, so a header or failure that only reading the content could produce may be missing.\n\nRequires the `member` role or higher.","operationId":"headDownloadFile","tags":["Files"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Absolute path inside the guest."},{"name":"no_wake","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Set to 1 to require a running computer without automatic resume: a computer that is not running answers `409` with `reason: unavailable` rather than being resumed. Any other value is a `400`. The dashboard always sends this."}],"responses":{"200":{"description":"The file, or the window you asked for.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"206":{"description":"The file, or the window you asked for.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"416":{"description":"Your `Range` named no byte the file has. The `Content-Range` on the refusal carries the file’s actual length, so you can ask again without guessing.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"put":{"summary":"Upload a file","description":"Writes a file inside the guest. The request body is the file itself, raw — not multipart, not JSON — and the destination is the `path` query parameter.\n\nOne upload is at most 64 MiB. A larger one is a `413` — refused on its `Content-Length` before any bytes move when the length is declared, and stopped at the limit otherwise. There is no ranged or append upload on this endpoint. Use `PUT /computers/{id}/files/uploads` for resumable files up to 2 GiB on Linux.\n\nBy default an existing file at `path` is replaced. Send `overwrite=false` to create the file only if nothing is there: a path that is taken answers `409` with `reason: \"exists\"` and nothing is written. The body is written beside the destination first and published only once it is complete, so incomplete contents are never published at `path`. A failure while publishing, or while answering, can still leave the complete file there: after an error, inspect `path` before retrying or overwriting, since what is there may be your own upload. Linux computers only: `overwrite=false` on Windows is a `400`. A computer whose host is too old to do a create-only upload answers `409` with `reason: \"unsupported\"` and nothing is sent; one whose host could not be asked answers `503`, and the same request can be sent again, since nothing was sent. Neither of those means the path is free.\n\nA computer suspended for being idle just as the upload arrived answers `409` with `resume_required` and `reason: \"contention\"`: send the entire upload again, and it resumes the computer first.\n\nRequires the `member` role or higher.","operationId":"uploadFile","tags":["Files"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Absolute path inside the guest."},{"name":"overwrite","in":"query","required":false,"schema":{"type":"string","enum":["true","false"],"default":"true"},"description":"`false` creates the file only if nothing is at `path`, and refuses with `409` (`reason: \"exists\"`) otherwise. The dashboard sends `false` and asks before replacing. Not supported on Windows."},{"name":"no_wake","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Set to 1 to require a running computer without automatic resume: a computer that is not running answers `409` with `reason: unavailable` rather than being resumed. Any other value is a `400`. The dashboard always sends this."}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary","description":"The file’s bytes."}}}},"responses":{"200":{"description":"Where it landed and how much of it did.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileWrite"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"path":"/home/user/notes.txt","bytes":20}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PUT \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/files?path=%2Fhome%2Fuser%2Fnotes.txt' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/octet-stream\" \\\n  --data-binary @notes.txt"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    query = {\"path\":\"/home/user/notes.txt\"}\n    data = Path(\"notes.txt\").read_bytes()\n    c = client.computers.get(id)\n    result = c.write_file(query[\"path\"], data)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { readFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst query = {\"path\":\"/home/user/notes.txt\"};\nconst bytes = await readFile(\"notes.txt\");\nconst c = await client.computers.get(id);\nconst result = await c.writeFile(query.path, bytes);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdFiles","tags":["Files"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/files/list":{"get":{"summary":"List a guest directory","description":"Reads directory entry names, types and regular-file sizes through the guest agent. Requires member access, like file downloads. The computer must already be running; listing never resumes it or extends its idle timer. The dashboard Files panel opens the guest user’s Desktop directory by default.\n\nSupported on Linux images with /usr/bin/python3. The fixed helper examines at most 512 entries and returns at most 128 KiB of JSON. A large directory returns truncated: true; the response is an unordered subset, not a page with a continuation token. Open a more specific directory by path to narrow the result.\n\nEntries are inspected without following symbolic links. A directory path whose final component is a symlink is refused. Control characters and non-UTF-8 filenames are omitted and counted in skipped. Metadata can change before a subsequent transfer; the existing file transfer path and size rules still apply. Clicking a name in the dashboard never executes a file.\n\nRequires the `member` role or higher.","operationId":"listGuestDirectory","tags":["Files"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Absolute directory path inside the guest."}],"responses":{"200":{"description":"A bounded directory listing, with explicit partial and skipped flags.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestDirectory"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"path":"/home/user","entries":[{"name":"notes.txt","type":"file","size_bytes":20},{"name":"Documents","type":"directory"}],"truncated":false,"skipped":0}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a `path` the guest cannot list: a directory that does not exist (`that directory no longer exists`), a path naming a regular file or a symbolic link (`that path is not an accessible directory`), or one the guest user may not read. Unlike the general rule above, these are not final: the same request succeeds once the directory exists or becomes readable. A download of a file that does not exist answers `404` instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/files/list?path=%2Fhome%2Fuser' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    query = {\"path\":\"/home/user\"}\n    c = client.computers.get(id)\n    result = c.list_directory(query[\"path\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst query = {\"path\":\"/home/user\"};\nconst c = await client.computers.get(id);\nconst result = await c.listDirectory(query.path);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List a guest directory (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headListGuestDirectory","tags":["Files"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Absolute directory path inside the guest."}],"responses":{"200":{"description":"A bounded directory listing, with explicit partial and skipped flags.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a `path` the guest cannot list: a directory that does not exist (`that directory no longer exists`), a path naming a regular file or a symbolic link (`that path is not an accessible directory`), or one the guest user may not read. Unlike the general rule above, these are not final: the same request succeeds once the directory exists or becomes readable. A download of a file that does not exist answers `404` instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdFilesList","tags":["Files"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/files/uploads":{"get":{"summary":"Read resumable upload status","description":"Read staged bytes and completion state before retrying an interrupted upload. Uses the guest agent and may resume the computer unless no_wake=1. A missing upload returns 404. This reads metadata; it does not publish a staged file.\n\nRequires the `member` role or higher.","operationId":"getFileUpload","tags":["Files"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Normalized absolute destination file path, at most 4096 bytes, with no control characters and no empty, `.` or `..` segment; the parent directory must exist. Anything else is a 400."},{"name":"upload_id","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{32}$"},"description":"A fresh random 32-character lowercase hex ID. Save it with path and the source file to resume."},{"name":"no_wake","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Set to 1 to require a running computer without automatic resume: a computer that is not running answers `409` with `reason: unavailable` rather than being resumed. Any other value is a `400`."}],"responses":{"200":{"description":"Current progress, including a receipt for a completed upload until DELETE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileUpload"},"examples":{"success":{"summary":"Illustrative upload response.","value":{"path":"/home/user/notes.txt","upload_id":"0123456789abcdef0123456789abcdef","offset":20,"size":20,"complete":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/files/uploads?path=%2Fhome%2Fuser%2Fnotes.txt&upload_id=0123456789abcdef0123456789abcdef' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    query = {\"path\":\"/home/user/notes.txt\",\"upload_id\":\"0123456789abcdef0123456789abcdef\"}\n    c = client.computers.get(id)\n    result = c.file_upload(query[\"path\"], query[\"upload_id\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst query = {\"path\":\"/home/user/notes.txt\",\"upload_id\":\"0123456789abcdef0123456789abcdef\"};\nconst c = await client.computers.get(id);\nconst result = await c.fileUpload(query.path, query.upload_id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read resumable upload status (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetFileUpload","tags":["Files"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Normalized absolute destination file path, at most 4096 bytes, with no control characters and no empty, `.` or `..` segment; the parent directory must exist. Anything else is a 400."},{"name":"upload_id","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{32}$"},"description":"A fresh random 32-character lowercase hex ID. Save it with path and the source file to resume."},{"name":"no_wake","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Set to 1 to require a running computer without automatic resume: a computer that is not running answers `409` with `reason: unavailable` rather than being resumed. Any other value is a `400`."}],"responses":{"200":{"description":"Current progress, including a receipt for a completed upload until DELETE.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"put":{"summary":"Upload a resumable file chunk","description":"Linux with /usr/bin/python3 only. Send raw chunks of at most 8 MiB in order. Each request body must arrive within two minutes, with no 60-second idle gap; use smaller chunks on slow links. Use the same path, upload_id, size and overwrite on each request. Total size is at most 2 GiB. The first chunk at offset 0 creates a private sibling staging directory. The final chunk publishes the complete file atomically; partial uploads never replace the destination. An empty chunk at offset=size retries publication. With overwrite=false an occupied destination returns 409 reason: exists. Offset or metadata conflicts return 409: read status before retrying. After a lost reply or partial write, GET status and resume from the returned offset. Never reuse an ID for different contents. Keep the source unchanged throughout. DELETE removes staging and the completion receipt without deleting the destination. Staging consumes guest disk until deleted; there is no automatic expiry. Completed data is retained as a hardlink, not a second copy, until the destination is replaced or deleted: from then on the receipt alone holds the old bytes, so DELETE it to free that space. Older hosts return 404; never fall back to ordinary PUT for a chunk.\n\nA chunk whose length differs from its Content-Length, or that runs past size from its offset, is a 400, and so is an empty chunk at any offset other than size. A Windows computer is a 400. A guest-side failure to write the chunk, such as a full guest disk or a directory the upload cannot write to, is a 400 naming the likely causes: fix it, then read status before retrying.\n\nA 503 can mean the host’s upload buffers are all in use by other uploads just then: nothing was staged, so read status and resend from its offset after a short pause. A computer suspended as the chunk arrived answers 409 with resume_required and reason: contention: read status, then resend from the returned offset.\n\nRequires the `member` role or higher.","operationId":"uploadFileChunk","tags":["Files"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Normalized absolute destination file path, at most 4096 bytes, with no control characters and no empty, `.` or `..` segment; the parent directory must exist. Anything else is a 400."},{"name":"upload_id","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{32}$"},"description":"A fresh random 32-character lowercase hex ID. Save it with path and the source file to resume."},{"name":"no_wake","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Set to 1 to require a running computer without automatic resume: a computer that is not running answers `409` with `reason: unavailable` rather than being resumed. Any other value is a `400`."},{"name":"offset","in":"query","required":true,"schema":{"type":"integer","minimum":0},"description":"The last confirmed byte offset, initially 0. At most `size`: a larger one is a 400."},{"name":"size","in":"query","required":true,"schema":{"type":"integer","minimum":0,"maximum":2147483648},"description":"Total file size in bytes."},{"name":"overwrite","in":"query","required":false,"schema":{"type":"string","enum":["true","false"],"default":"true"},"description":"Replace the destination atomically when complete, or refuse if it exists."}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary","description":"At most 8 MiB of raw file bytes."}}}},"responses":{"200":{"description":"Confirmed progress and publication state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileUpload"},"examples":{"success":{"summary":"Illustrative upload response.","value":{"path":"/home/user/notes.txt","upload_id":"0123456789abcdef0123456789abcdef","offset":20,"size":20,"complete":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PUT \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/files/uploads?path=%2Fhome%2Fuser%2Fnotes.txt&upload_id=0123456789abcdef0123456789abcdef&offset=0&size=20&overwrite=true' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/octet-stream\" \\\n  --data-binary @notes.txt"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom pathlib import Path\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    query = {\"path\":\"/home/user/notes.txt\",\"upload_id\":\"0123456789abcdef0123456789abcdef\",\"offset\":\"0\",\"size\":\"20\",\"overwrite\":\"true\"}\n    data = Path(\"notes.txt\").read_bytes()\n    c = client.computers.get(id)\n    result = c.write_file_chunk(query[\"path\"], data, upload_id=query[\"upload_id\"], offset=int(query[\"offset\"]), size=int(query[\"size\"]))\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\nimport { readFile } from \"node:fs/promises\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst query = {\"path\":\"/home/user/notes.txt\",\"upload_id\":\"0123456789abcdef0123456789abcdef\",\"offset\":\"0\",\"size\":\"20\",\"overwrite\":\"true\"};\nconst bytes = await readFile(\"notes.txt\");\nconst c = await client.computers.get(id);\nconst result = await c.writeFileChunk(query.path, bytes, {uploadId: query.upload_id, offset: Number(query.offset), size: Number(query.size)});\nconsole.log(JSON.stringify(result, null, 2));"}]},"delete":{"summary":"Remove resumable upload staging","description":"Cancel an interrupted upload or clean up its completion receipt. Idempotent when the staging directory is absent. Never deletes the published destination. May resume the computer unless no_wake=1.\n\nRequires the `member` role or higher.","operationId":"cancelFileUpload","tags":["Files"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"path","in":"query","required":true,"schema":{"type":"string"},"description":"Normalized absolute destination file path, at most 4096 bytes, with no control characters and no empty, `.` or `..` segment; the parent directory must exist. Anything else is a 400."},{"name":"upload_id","in":"query","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{32}$"},"description":"A fresh random 32-character lowercase hex ID. Save it with path and the source file to resume."},{"name":"no_wake","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Set to 1 to require a running computer without automatic resume: a computer that is not running answers `409` with `reason: unavailable` rather than being resumed. Any other value is a `400`."}],"responses":{"200":{"description":"Staging removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"Illustrative upload response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"502":{"description":"The agent inside the guest is not answering, or it rejected or malformed the private command protocol for this feature. The computer is up; the upstream agent interaction failed. This is not malformed caller input and carries no retry classification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/files/uploads?path=%2Fhome%2Fuser%2Fnotes.txt&upload_id=0123456789abcdef0123456789abcdef' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    query = {\"path\":\"/home/user/notes.txt\",\"upload_id\":\"0123456789abcdef0123456789abcdef\"}\n    c = client.computers.get(id)\n    result = c.cancel_file_upload(query[\"path\"], query[\"upload_id\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst query = {\"path\":\"/home/user/notes.txt\",\"upload_id\":\"0123456789abcdef0123456789abcdef\"};\nconst c = await client.computers.get(id);\nconst result = await c.cancelFileUpload(query.path, query.upload_id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdFilesUploads","tags":["Files"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/snapshots":{"get":{"summary":"List snapshots","description":"Every snapshot on the account that you can act on. Snapshots outlive the computers they came from, so this list routinely contains rows whose `computer_id` resolves to nothing — those carry `orphaned: true`, and `clone` is the operation that still works on them.\n\nUncertain fleet computer presence can require `allow_partial=true` even when snapshot rows were read; those rows carry `computer_unreachable` instead of a confirmed orphan status.\n\nCAPTURES IN PROGRESS ARE LISTED TOO, and they are not snapshots yet. Such a row reads `state: \"capturing\"`: restore, clone and delete all answer 404 on it until it lands. It carries the id the finished snapshot will have, which is what makes this endpoint the one you poll after `POST /computers/:id/snapshots` — the row you were handed there is this row, and it stops reading `capturing` in place rather than being replaced by something under another id. A capture appears exactly once: while it runs you get the placeholder, and from the moment the snapshot is stored you get the snapshot, never both.\n\nCHECK `state` ON EVERY ROW, not on the newest one. This listing is a concatenation of one answer per host your computers are on, in a fixed host order that has nothing to do with time, so it carries no account-wide ordering to read anything from: a capture running on one host can appear after finished snapshots from another, and will do so consistently rather than intermittently.\n\nRequires the `viewer` role or higher.","operationId":"listSnapshots","tags":["Snapshots"],"x-rate-cost":5,"parameters":[{"name":"allow_partial","in":"query","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Accept a listing known to be short. Without it this endpoint answers 503 when a hypervisor holding some of your things cannot be reached, because a short list is not a smaller truth — it reads exactly like the missing ones were deleted, and the obvious next thing a script does with something that has disappeared is tidy it up. A snapshot on a host that cannot be reached is listed as its `id` and `unreachable: true`, and nothing else; a key scoped to a workspace gets no such rows, only the short answer. A row whose source computer cannot be found for certain carries `computer_unreachable: true` and no `orphaned`."},{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["unfinished"]},"description":"Widen the listing to snapshots whose deletion began and has not finished — their `state` reads `deleting`. Left out by default because a half-deleted snapshot is not one you can restore or clone, so a bare listing is the set you can actually act on. Poll with it to learn when a deletion has finished: the row goes then."}],"responses":{"200":{"description":"Your snapshots — every one you can act on, plus unfinished deletions if you asked for them.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Snapshot"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"snap-c1d2e3f4a5b6","computer_id":"vm-a1b2c3d4e5f6","computer_name":"scratch","orphaned":false,"name":"before-upgrade","kind":"memory","state":"durable","size_bytes":1073741824,"created_at":"2026-09-16T12:00:00.000Z","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"resolution":"1280x800x24","auto":false,"incremental":false,"restore_available":true}]}}}},"headers":{"X-GC-Incomplete":{"description":"Present only on a listing known to be short, which is answered only when `allow_partial` was passed: a host did not answer. Its value is how many rows the answer carries as `unreachable: true` — snapshots the platform knows of whose host did not answer, with nothing else on them. It can be `0`, which means the listing may be short by snapshots the platform cannot name.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-GC-Incomplete":{"description":"Present when this `503` is the refusal of a short listing; pass `allow_partial=1` to be answered with the short listing instead. Its value is how many rows the answer carries as `unreachable: true` — snapshots the platform knows of whose host did not answer, with nothing else on them. It can be `0`, which means the listing may be short by snapshots the platform cannot name.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/snapshots' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.snapshots.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.snapshots.listWithStatus();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List snapshots (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListSnapshots","tags":["Snapshots"],"parameters":[{"name":"allow_partial","in":"query","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Accept a listing known to be short. Without it this endpoint answers 503 when a hypervisor holding some of your things cannot be reached, because a short list is not a smaller truth — it reads exactly like the missing ones were deleted, and the obvious next thing a script does with something that has disappeared is tidy it up. A snapshot on a host that cannot be reached is listed as its `id` and `unreachable: true`, and nothing else; a key scoped to a workspace gets no such rows, only the short answer. A row whose source computer cannot be found for certain carries `computer_unreachable: true` and no `orphaned`."},{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["unfinished"]},"description":"Widen the listing to snapshots whose deletion began and has not finished — their `state` reads `deleting`. Left out by default because a half-deleted snapshot is not one you can restore or clone, so a bare listing is the set you can actually act on. Poll with it to learn when a deletion has finished: the row goes then."}],"responses":{"200":{"description":"Your snapshots — every one you can act on, plus unfinished deletions if you asked for them.","headers":{"X-GC-Incomplete":{"description":"Present only on a listing known to be short, which is answered only when `allow_partial` was passed: a host did not answer. Its value is how many rows the answer carries as `unreachable: true` — snapshots the platform knows of whose host did not answer, with nothing else on them. It can be `0`, which means the listing may be short by snapshots the platform cannot name.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-GC-Incomplete":{"description":"Present when this `503` is the refusal of a short listing; pass `allow_partial=1` to be answered with the short listing instead. Its value is how many rows the answer carries as `unreachable: true` — snapshots the platform knows of whose host did not answer, with nothing else on them. It can be `0`, which means the listing may be short by snapshots the platform cannot name.","schema":{"type":"string","pattern":"^[0-9]+$"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSnapshots","tags":["Snapshots"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/snapshots":{"get":{"summary":"Get what a computer holds","description":"Complete, scoped fleet holdings for this computer, including physical duplicates, captures and unfinished deletions. Returns an opaque fingerprint for DELETE consent and current computer presence. A host that cannot be asked, or has no configured store, is left out only when nothing of yours can be on it; otherwise this returns 503 instead of a total. That 503 carries `found` (`count`, `size_bytes`): what the hosts that answered hold, a lower bound with no fingerprint, so it can never confirm a purge. A computer being moved between hosts returns 409 with `reason: \"contention\"`; wait for the move to finish. Two hosts listing it with no move under way returns the same 409 with no `reason`: a copy support has to settle. A changed fleet returns 409 with no `reason`; refresh the confirmation. Works after computer deletion when authoritative scoped ownership or scoped snapshot evidence remains. The presentation list GET /snapshots may merge duplicate IDs and cannot reconstruct this fingerprint.\n\nRequires the `viewer` role or higher.","operationId":"getSnapshotHoldings","tags":["Snapshots"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Holdings"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"count":1,"size_bytes":1073741824,"fingerprint":"fleet-v1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","computer_present":true,"capturing":0,"deleting":0}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"found":{"type":"object","properties":{"count":{"type":"integer","description":"Copies the hosts that answered hold."},"size_bytes":{"type":"integer","description":"Their total size in bytes."}},"required":["count","size_bytes"],"description":"Present when some hosts answered and others could not be asked: a lower bound, with no fingerprint, so it can never confirm a purge."}}}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/snapshots' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.snapshot_holdings()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.holdings();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get what a computer holds (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetSnapshotHoldings","tags":["Snapshots"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The summary.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"post":{"summary":"Take a snapshot","description":"Captures the disk. Pass `memory: true` to capture the running session with it, so a restore comes back to a live desktop rather than to a boot.\n\nTHE CAPTURE OUTLIVES THIS CALL. You get a `202` and a row in `state: \"capturing\"` the moment the capture is accepted, not when it finishes — copying a disk takes minutes and scales with how much has been written to it, which is longer than any HTTP request survives. Poll `GET /snapshots` for the id you were given and wait for the row to STOP READING `capturing`.\n\nMANUAL CAPTURES COUNT STORED BYTES AND CAPTURES ALREADY IN PROGRESS across the account. A capture can start while usage is below the allowance even if its eventual size crosses it; later captures are refused with 402 until usage is back under the allowance. A capture requested while another manual capture on the account is still being admitted answers 503 — retry. If acceptance cannot be confirmed, check the snapshot list before retrying. Admission also answers `503` (“Snapshot admission could not be confirmed…”) whenever any host in the fleet cannot confirm what snapshots it holds — one that is down or not answering, one whose snapshot storage is failing, or one running software too old to confirm it — even a host that holds nothing of yours; it lasts as long as that host’s state does. Automatic (scheduled) captures count toward the allowance too, so they can be what brings an account to the `402` — and a scheduled capture can be skipped, rather than taken, when the allowance is full, or on a new account or a computer newly placed on its host until the next complete hourly measurement of the allowance reaches that host. See `PUT /computers/{id}/schedule`.\n\nWAIT FOR “no longer capturing”, NOT FOR `pending` SPECIFICALLY. `pending` is where a finished capture lands, but replication to backup storage can complete between two of your polls and carry it straight on to `durable` — a loop waiting for the literal string `pending` can watch a small snapshot go past and never match. Both are states you can restore, clone and delete from.\n\nONE CAPTURE OF A COMPUTER AT A TIME, AND IT LASTS UNTIL THE UPLOAD. A capture holds its computer until its upload to backup storage has finished, which is later than when it stops reading `capturing`. Another capture of the SAME computer in that window — yours, or a scheduled one — is refused with `409` and `reason: \"contention\"`; retry, and it is accepted once the upload has finished, which is ordinarily when the previous row reads `durable`. Captures of other computers are not held up by it.\n\nTHE ID YOU ARE GIVEN IS THE SNAPSHOT’S OWN. It does not change when the capture lands, so it is what you poll on — never \"the newest snapshot of this computer\", which a scheduled capture finishing in the same window gets wrong.\n\nEVERYTHING THAT CAN REFUSE A CAPTURE IS REFUSED HERE, before the `202`: no such computer (`404`), a disk still being copied (`409`), a computer being deleted (`409`), one that has not yet finished taking on an identity of its own, such as a new computer that has never been started (`400`; start it, let it come up, and try again), a host with no snapshot storage configured (`400`), a capture of this computer still holding it (`409`, `reason: \"contention\"`; see above), a memory snapshot of a computer that is not running (`409`, `reason: \"unavailable\"`; start it), an allowance that will not stretch (`402`), a computer the platform is holding for disk-space recovery after a suspend or memory snapshot failed (`503`, “waiting for disk-space recovery”; wait, or stop it to discard the paused session), and three that only a memory snapshot of a computer holding secrets meets: its secrets still being delivered (`409`, `reason: \"contention\"`, and worth retrying — it clears in a moment; `reason: \"unsupported\"` instead for a session they were never delivered to and that nothing is delivering to now, which a stop and a start clears; for such a session, `reason: \"contention\"` when its host could not read whether it is running just now, worth sending again shortly, and `reason: \"unsupported\"` when its host cannot read its process until support repairs the host), a session that may still hold secret values it is no longer bound to (`409` until the computer restarts), and a host that does not seal saved memory (`409`; a disk snapshot is the way out). A `202` means the capture started, not that it will land: a capture waits its turn behind others on the same host, and when that turn comes a memory capture checks again that the computer is running and the three secrets refusals above, and, on a computer holding secrets, that its guest says its secrets reached the boot it is running and that it does not reboot during the capture. So one already accepted can still end if the computer stops, a delivery starts or its bindings change while it waits, and on such a computer if it reboots or its guest does not answer. Any capture can also end after the `202` when the host does not have enough unreserved disk space for the copy, and a memory capture that fails while the host is low on disk space can leave the computer paused, still reading `running` (see `status` on Computer). A capture that then fails leaves no snapshot and no row — the `capturing` row disappears and nothing takes its place, which is how you tell that from one still running.\n\nRequires the `member` role or higher.","operationId":"createSnapshot","tags":["Snapshots"],"x-rate-cost":50,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Defaults to the computer’s name, a space, and the UTC time the copy was taken as `YYYY-MM-DD HH:MM`. When you do not send one, the `capturing` row this call returns has an empty `name` until the capture lands."},"memory":{"type":"boolean","description":"Include the running session. The computer must be running, and one holding secrets must have finished receiving them, must not have had its bindings changed while it ran, and must be on a host that seals saved memory — each a `409` at admission otherwise, and a capture already accepted can still end afterwards for the same reasons; see above."}}}}}},"responses":{"202":{"description":"The capture, accepted — a placeholder row carrying the id the snapshot will have.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Snapshot"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"snap-c1d2e3f4a5b6","computer_id":"vm-a1b2c3d4e5f6","computer_name":"scratch","orphaned":false,"name":"before-upgrade","kind":"memory","state":"capturing","size_bytes":0,"created_at":"2026-09-16T12:00:00.000Z","os":"","template":"","cpu":0,"ram_mb":0,"disk_gb":0,"resolution":"1280x800x24","auto":false,"incremental":false}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/snapshots' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"before-upgrade\",\"memory\":true}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"name\\\":\\\"before-upgrade\\\",\\\"memory\\\":true}\")\n    c = client.computers.get(id)\n    result = c.snapshot(**body, wait=False)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"name\":\"before-upgrade\",\"memory\":true};\nconst c = await client.computers.get(id);\nconst result = await c.snapshot({...body, wait: false});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdSnapshots","tags":["Snapshots"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/snapshots/{id}/restore":{"post":{"summary":"Restore a snapshot","description":"Puts this snapshot back onto the computer it came from, replacing its current disk. Needs that computer to still exist — an orphaned snapshot cannot be restored, only cloned.\n\nA snapshot held by two hosts at once — a move part way through handing its history over — is served from the copy on the host its computer now lives on. One that cannot be attributed that way, such as an orphan, answers 409 until the handover finishes; nothing is changed.\n\nA snapshot taken before its computer moved to another host (`POST /computers/{id}/move`) is carried to the new host after the move. Until it lands, restoring it answers 409 with `reason: \"contention\"`, saying it is still being carried over; try again in a few minutes. If the carry failed, the 409, with no `reason`, says it is still on the host the computer moved from, and that does not clear on its own: clone it instead, or ask support to move it. Nothing is changed either way. Cloning the snapshot works while it is being carried, except while the new host is taking it over, when a clone can answer `409` as well; retry once the carry has finished.\n\nIT LEAVES THE COMPUTER RUNNING, whatever state it was in. A restore of a stopped computer boots it, and that is a start like any other: charged, and refusable by your plan. A disk snapshot comes back to a fresh boot; a memory one resumes the captured session. Either way any suspended session the computer was holding is discarded, since it was saved against the disk being replaced. On a host without enough free disk space for it, a restore answers `503` before anything is touched. If space runs short after the disk has been replaced, the start at the end can still be refused with `503` (“this host is low on disk space…”), and the computer is left stopped on the restored disk: start it once space there has been freed. If that message adds that its paused QEMU could not be stopped, stop the computer before trying again.\n\nA computer that holds secrets restored from a DISK snapshot gets them delivered afresh at that boot, with their current values, exactly as a start delivers them; the snapshot never carried them. This delivery has the same workspace-conflict refusals and recovery as `POST /computers/{id}/start`. One restored from a memory snapshot resumes holding what it held when the snapshot was taken. A memory snapshot taken without sealing is refused onto a computer that now holds secrets (`409`, `reason: \"unsupported\"`), and a sealed one with `400` and no `reason` when the platform will not open its saved memory for this computer, for example because a secret it held has since been deleted or moved out of the computer’s scope. Both are checked before anything is stopped, and then nothing is stopped or replaced. The saved memory is opened again when it loads, so a refusal that arrives only then — a secret deleted in the meantime, say — ends the restore after the disk has been replaced, leaving the computer stopped.\n\nA MEMORY SNAPSHOT ONLY LOADS INTO THE SHAPE IT CAME OFF. Resize the computer after capturing one and the restore is refused — the vCPU count and the memory size are part of a saved memory image, not decoration around it. The refusal names the shape to go back to; `clone` is the other way out: a clone resumes the session in a new computer at the captured shape — for a snapshot of a computer that held secrets only with `inherit_secrets: true`, and otherwise it is built from the disk and says so with `memory_dropped: true` — or, with `memory: false`, is built from the disk alone; one built from the disk lands stopped and boots fresh when you start it (see `POST /snapshots/{id}/clone`). Refused before anything is taken down, so a computer that gets this answer is untouched.\n\nRequires the `member` role or higher.","operationId":"restoreSnapshot","tags":["Snapshots"],"x-rate-cost":100,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The snapshot id."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"responses":{"200":{"description":"Restored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LifecycleAck"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.\n\nAlso, for a computer that holds secrets restored from a disk snapshot, the secret refusals of `POST /computers/{id}/start`, with its words: `contention` while the computer is being moved to another workspace or a change to its secrets is still being saved, `unsupported` for a workspace change whose outcome was lost, bindings recorded in a workspace the computer is no longer in, a host list of bindings that does not match the recorded one, a bound secret that is gone or cannot be delivered, or secrets delivery not switched on for this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso a sealed session the platform could not open, seal or carry just then — the service that holds its key did not answer, or the host failed moving the saved session — which is the platform’s fault and not the request’s. Read the computer’s status, then send the restore again; it usually clears in a moment. Sent with an `Idempotency-Key`, sending it again under the same key is safe: when the host refused it before changing anything, its key was released and the answer names no `operation_id`, so the resend is carried out; when the answer names an `operation_id`, the outcome is unknown and the resend answers `409 idempotency_outcome_unknown` — read the computer and that operation instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/snapshots/snap-c1d2e3f4a5b6/restore' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"snap-c1d2e3f4a5b6\"\n    result = client.snapshots.restore(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"snap-c1d2e3f4a5b6\";\nconst result = await client.snapshots.restore(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSnapshotsByIdRestore","tags":["Snapshots"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The snapshot id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/snapshots/{id}/clone":{"post":{"summary":"Clone a snapshot into a new computer","description":"Builds a new computer from this snapshot. Works on an orphan, which is what makes deleting a computer and keeping its snapshots a recoverable decision.\n\nA snapshot held by two hosts at once — a move part way through handing its history over — is served from the copy on the host its computer now lives on. One that cannot be attributed that way, such as an orphan, answers 409 until the handover finishes; nothing is changed.\n\nThe answer arrives as soon as the new computer exists, with `status: \"building\"` while the snapshot is copied out — poll it, or wait on its `operation_id`. A clone BUILT FROM DISK — a disk snapshot, `memory: false`, or one answered with `memory_dropped: true` — lands STOPPED, and boots fresh when you start it; a caller waiting for `running` without starting it waits for ever. A memory fork resumes by itself once it is built.\n\nA MEMORY snapshot is resumed: the new computer comes up running the captured session. Pass `memory: false` to build it from the snapshot's disk alone instead, with its own network identity, landing stopped — the way out when the saved session is what is broken.\n\nA snapshot whose workspace has since been deleted is cloned into no workspace; only an account-wide key reaches it. Its saved session cannot be resumed there, so a clone of a memory snapshot that would resume it (`memory` absent, `null` or `true`) is refused with `409` and nothing is created. Send `memory: false` to build it from its disk instead; the new computer is in no workspace.\n\nA memory snapshot of a computer that held secrets is resumed into a new computer only if you pass `inherit_secrets: true`: its saved memory holds the delivered values, so the new computer comes up holding the same credentials, bound to the same secrets at the revisions it holds, and gets current values at its next reboot. It lands in the same workspace, and is refused its session if any of those secrets has since been deleted or moved out of scope. Without it — or for a snapshot taken before this could be recorded — the clone is built from the disk and the response says so with `memory_dropped: true`; check for it before assuming the session came across.\n\nA resumed copy is given an identity of its own before its network comes up: its own MAC and address, its name as hostname, a new machine ID and SSH host keys, and its own desktop password, so it runs beside its source. The session is otherwise untouched. If that cannot be done the copy is stopped rather than left running as its source; starting it boots its disk fresh.\n\nRequires the `member` role or higher.","operationId":"cloneSnapshot","tags":["Snapshots"],"x-rate-cost":100,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The snapshot id."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[!-~]+$"},"description":"Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so `GET /operations?idempotency_key=` finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers `409 idempotency_in_progress` while the first is running, and the original answer — status and body, with `Idempotent-Replayed: true` — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a `200` carrying `{operation_id, operation}`, also with `Idempotent-Replayed: true`: `operation` is the call’s operation, or `null` when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (`vnc`, with its tokens and URLs): read the computer (`GET /computers/{id}`) for them. The same key on a different request is a `422` with `code: \"idempotency_key_reused\"`, and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a `400` with `code: \"idempotency_key_invalid\"`; neither does anything. A call refused with a `4xx` that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose `4xx` says the computer was deleted — `computer_deleted: true`, or a `purge` result beside `computer_deleted: false` (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is `succeeded`. Except a `409` with `reason: \"contention\"` whose `purge` deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With `computer_deleted: null`, or `false` where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers `409 idempotency_in_progress` until the platform gives up on it, then `409 idempotency_outcome_unknown`. A `5xx` answer that names an `operation_id` is an unknown outcome too: that is the operation reserved for it. From then on the same key answers `409 idempotency_outcome_unknown`, naming that operation (not `idempotency_in_progress`): read the computer, or the operation. The operation stays `pending` until the platform gives up on it; if it never learns how the call ended, it becomes `failed` with `error.code: \"lost\"`. Some `5xx` answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a `503` from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, `suspend_outcome: \"not_started\"`). Those name no `operation_id`: nothing was done, so the key is released as a `4xx` refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its `5xx` is an unknown outcome like any other. A `5xx` without an `operation_id` is not proof that nothing was done on its own: a call that records no operation (a `PATCH` that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any `5xx`: it is carried out if its key was released, and answered `409 idempotency_outcome_unknown` if not."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Name for the new computer. Defaults to the snapshot’s name followed by `-restored`. Cut to 64 characters, with control characters turned into spaces and the ends trimmed. A name left empty by that cleaning gets the default too."},"memory":{"type":"boolean","description":"Memory snapshots only: `false` builds from the disk alone; it lands stopped and boots fresh when started. Defaults to `true`. Ignored for a disk snapshot. A memory snapshot whose workspace was deleted needs `false`; see above."},"inherit_secrets":{"type":"boolean","description":"Consent to resuming a memory snapshot of a computer that held secrets: the new computer holds the same credentials. Ignored for a snapshot that holds none, and when `memory` is `false`."}}}}}},"responses":{"201":{"description":"The new computer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotClone"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"vm-c3d4e5f6a7b8","name":"from-snapshot","status":"building","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"resolution":"1280x800x24","created_at":"2026-09-16T12:00:00.000Z","vnc":{"url":"wss://app.mandala.computer/api/v1/computers/vm-c3d4e5f6a7b8/vnc?token=illustrative-control-token","view_url":"wss://app.mandala.computer/api/v1/computers/vm-c3d4e5f6a7b8/vnc?token=illustrative-view-token","token":"illustrative-control-token","view_token":"illustrative-view-token","terminal_url":"wss://app.mandala.computer/api/v1/computers/vm-c3d4e5f6a7b8/terminal?token=illustrative-control-token","events_url":"wss://app.mandala.computer/api/v1/computers/vm-c3d4e5f6a7b8/events?token=illustrative-control-token","embed_url":"https://app.mandala.computer/embed/desktop#computer=vm-c3d4e5f6a7b8&token=illustrative-view-token","clipboard":true},"operation_id":"op_0123456789abcdef01234567"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Idempotent-Replayed":{"description":"`true` when this is the stored answer to an earlier call with the same `Idempotency-Key`, not a new one. Absent otherwise.","schema":{"type":"string","enum":["true"]}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.\n\nAlso a malformed `Idempotency-Key`, with `code: \"idempotency_key_invalid\"`: empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso, with no `reason`, `Verify your email before launching a computer.`: the person this credential belongs to has not verified their email address. Nothing is created; verify it in the dashboard first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nWith an `Idempotency-Key`, two more. `code: \"idempotency_in_progress\"`, with `reason: \"contention\"`, `Retry-After` and the `operation_id`: the call with this key is still being carried out — send it again shortly for its answer. `code: \"idempotency_outcome_unknown\"`, with no `reason`: the platform did not hear how the call with this key ended (as after a `5xx` answer to it that named an `operation_id`), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.\n\nAlso a memory snapshot whose workspace has since been deleted, cloned to resume its session (`memory` absent, `null` or `true`): no `reason`, nothing is created, and sending it again does not help. Send `memory: false` to build it from its disk; the new computer is in no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before resending. On `idempotency_in_progress`, send the same call again for its answer; on a create’s `template_image_preparing`, repeat the create with its `template_transfer` token.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"422":{"description":"The `Idempotency-Key` on this request was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done: a key stands for one request, so send this one under a new key.\n\nThis `Idempotency-Key` was used within its 24 hours with a different request (`code: \"idempotency_key_reused\"`). Nothing was done; send this request under a new key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).\n\nAlso, with `inherit_secrets: true`, when 16 forks of this snapshot are already waiting to be given its secrets. This one is not about your rate budget: it carries `reason: \"contention\"` and `Retry-After`, and nothing is created. The wait is the time until the earliest pending grant expires, rounded up to whole seconds and bounded to 1–1800 seconds. A waiting fork stops counting sooner once it has been given the secrets, once its clone is refused or built from the disk instead; otherwise its grant expires 30 minutes after it was requested. A slot can therefore open sooner, and the wait does not reserve one against another caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso when the snapshot source cannot be confirmed; no fork is created and no grant is recorded.\n\nAlso a sealed session the platform could not open, seal or carry just then — the service that holds its key did not answer, or the host failed moving the saved session — which is the platform’s fault and not the request’s. Read the computer’s status, then send the clone again; it usually clears in a moment. Sent with an `Idempotency-Key`, sending it again under the same key is safe: when the host refused it before changing anything, its key was released and the answer names no `operation_id`, so the resend is carried out; when the answer names an `operation_id`, the outcome is unknown and the resend answers `409 idempotency_outcome_unknown` — read the computer and that operation instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/snapshots/snap-c1d2e3f4a5b6/clone' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"from-snapshot\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"snap-c1d2e3f4a5b6\"\n    body = json.loads(\"{\\\"name\\\":\\\"from-snapshot\\\"}\")\n    result = client.snapshots.clone(id, body[\"name\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"snap-c1d2e3f4a5b6\";\nconst body = {\"name\":\"from-snapshot\"};\nconst result = await client.snapshots.clone(id, body.name);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSnapshotsByIdClone","tags":["Snapshots"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The snapshot id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/snapshots/{id}":{"delete":{"summary":"Delete a snapshot","description":"Deleting a link in an incremental chain does not lose the snapshots that depend on it — the daemon keeps what they need.\n\nA snapshot held by two hosts at once — a move part way through handing its history over — is served from the copy on the host its computer now lives on. One that cannot be attributed that way, such as an orphan, answers 409 until the handover finishes; nothing is changed.\n\nTHE DELETION OUTLIVES THIS CALL. You get a `202` and the snapshot’s row the moment the deletion is accepted, not when it finishes — detaching those dependents, then removing the stored objects, takes time that scales with the chain and with how much is stored, which is longer than any HTTP request survives. Poll `GET /snapshots?include=unfinished` for the id and WAIT FOR THE ROW TO GO. Its absence there is the deletion having finished; there is no state that means \"deleted\". A bare `GET /snapshots` drops the row as soon as it reads `deleting`, which can be long before the deletion has finished, so absence from a bare listing says only that the snapshot can no longer be restored or cloned.\n\nA ROW THAT STAYS SAYS WHICH OF TWO THINGS HAPPENED, by its state, and it is the opposite polarity to a failed capture, which leaves no row at all. Once the daemon has detached the dependents it records the deletion and marks the snapshot `deleting`, and that state is left out of a bare listing because a half-deleted snapshot is not one you can restore or clone. A `deleting` row finishes without you: one held up by a backup upload of this snapshot that is slow to stop reads `deleting` while it waits and finishes once the upload has stopped, and one that stalls after that is retried by the daemon itself. A row still in the state it had has not recorded the deletion. It may still be working: detaching its dependents, or waiting on a slow upload while backup storage also refuses the record, which it goes on trying to write. Or it failed before recording anything (a dependent that is itself being deleted, below, or backup storage refusing the record with no upload to wait on), and then nothing was destroyed and nothing retries it. A row that stays in its old state is therefore worth a second `DELETE`: it is refused with a `409` while another deletion of this snapshot holds it, and otherwise accepted as one more attempt, which does no harm if an earlier one is still waiting; a `202` does not say that the first one failed.\n\nEVERYTHING THAT CAN REFUSE A DELETION OF THIS SNAPSHOT IS REFUSED HERE, before the `202`: no such snapshot, a capture reading through it, a clone or a migration holding it, and a deletion of this snapshot already running. That last one is what a second `DELETE` gets for as long as the first is working, so it is an answer about progress rather than a fault. Each of those `409`s ends by itself and carries `reason: \"contention\"`.\n\nONE CONFLICT ARRIVES AFTER THE `202`, and it is about a different snapshot: a dependent that is ITSELF being deleted cannot be detached, so deleting the link it hangs off fails once the work starts. Nothing is destroyed — the row stays exactly as it was, in its ordinary state rather than `deleting` — and the delete succeeds once the dependent’s own deletion has finished. Deleting a chain one link at a time, waiting for each row to go, never meets it.\n\nRequires the `member` role or higher.","operationId":"deleteSnapshot","tags":["Snapshots"],"x-rate-cost":10,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The snapshot id."}],"responses":{"202":{"description":"The deletion, accepted — the row that goes when it has finished. It omits `orphaned`, which this answer does not work out; `GET /snapshots` carries it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Snapshot"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"snap-c1d2e3f4a5b6","computer_id":"vm-a1b2c3d4e5f6","computer_name":"scratch","name":"before-upgrade","kind":"memory","state":"durable","size_bytes":1073741824,"created_at":"2026-09-16T12:00:00.000Z","os":"linux","template":"base","cpu":2,"ram_mb":2048,"disk_gb":20,"resolution":"1280x800x24","auto":false,"incremental":false}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/snapshots/snap-c1d2e3f4a5b6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"snap-c1d2e3f4a5b6\"\n    result = client.snapshots.delete(id, wait=False)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"snap-c1d2e3f4a5b6\";\nconst result = await client.snapshots.delete(id, {wait: false});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSnapshotsById","tags":["Snapshots"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The snapshot id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/schedule":{"get":{"summary":"Get the snapshot schedule","description":"The window automatic snapshots are taken in. There is deliberately no \"last run\" here: the scheduler’s own bookkeeping reads like backup history and lies in both directions. Snapshots carry real capture times — read `GET /snapshots` for a freshness check.\n\nA computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot, so an old newest snapshot on one is not a missed window.\n\nThis says when they are TAKEN and not how long they survive. `GET /retention` is the other half.\n\nRequires the `viewer` role or higher.","operationId":"getSchedule","tags":["Schedules"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schedule"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"enabled":true,"hour":3,"minute":30,"tz":"Europe/London"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/schedule' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.schedule()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.schedule();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get the snapshot schedule (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetSchedule","tags":["Schedules"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The schedule.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"put":{"summary":"Set the snapshot schedule","description":"How long these are kept is your plan’s retention, not a field here — read it at `GET /retention`, which is what tells you how many of the snapshots this schedule takes will still be there next month.\n\nThe PUT replaces the whole window: `hour` and `minute` default to 0 and `tz` to UTC, so send all three again when only toggling `enabled`. A computer being copied or moved answers 409; send it again once that finishes.\n\nA computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot, so an old newest snapshot on one is not a missed window.\n\nAUTOMATIC CAPTURES COUNT AGAINST YOUR SNAPSHOT STORAGE ALLOWANCE, and can be skipped when it is full. Their bytes count toward the same allowance a manual capture does, so they can bring an account to the point where a manual capture answers `402`; and a scheduled capture skipped for storage is not taken. Nothing is deleted to make room for it. It is retried through the rest of that day and is taken once there is room — retention ageing out an older automatic snapshot, or you deleting one. Room is measured across every computer on the account, normally once an hour, and each host is sent its share. A measurement is sent only when every host answers it, and until a complete one reaches a host, that host keeps the share it was last sent. So a skipped capture usually waits up to an hour after room appears, longer while any host cannot be measured, and one can still land shortly after the allowance fills. Each plan’s allowance is sized to hold the history its retention keeps, so a schedule meets it only when manual snapshots or unusually busy disks have used that room.\n\nA NEW ACCOUNT OR A NEWLY PLACED COMPUTER CAN ALSO BE SKIPPED UNTIL THE NEXT COMPLETE MEASUREMENT. Each host’s share of the room left in the allowance is in proportion to the account’s computers on it. Until a complete measurement reaches the host, a new account has no share anywhere, and a computer created on or moved to a host where the account had no computers has none there, so its scheduled capture is skipped even with nothing stored. That is normally within the hour, but a host that cannot be measured, or a share that cannot be delivered, can stretch it to hours or days. A skipped capture is retried through the rest of that day like any other, so a longer wait can cost that day’s capture.\n\nOne automatic capture a day, and an edit does not add another. Changing the time or `tz` after today’s capture has been taken moves the window from tomorrow on; it does not open a second one today, and neither does removing the schedule and setting it again in the same `tz`.\n\nRequires the `member` role or higher.","operationId":"setSchedule","tags":["Schedules"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"hour":{"type":"integer","description":"0–23, in `tz`. 0 when left out.","minimum":0,"maximum":23},"minute":{"type":"integer","description":"0–59. 0 when left out.","minimum":0,"maximum":59},"tz":{"type":"string","description":"IANA zone name, e.g. `Europe/London`. Defaults to UTC. A name the host’s zone database does not know is a `400`."}},"required":["enabled"]}}}},"responses":{"200":{"description":"The schedule as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schedule"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"enabled":true,"hour":3,"minute":30,"tz":"Europe/London"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PUT \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/schedule' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"enabled\":true,\"hour\":3,\"minute\":30,\"tz\":\"Europe/London\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"enabled\\\":true,\\\"hour\\\":3,\\\"minute\\\":30,\\\"tz\\\":\\\"Europe/London\\\"}\")\n    c = client.computers.get(id)\n    result = c.set_schedule(**body)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"enabled\":true,\"hour\":3,\"minute\":30,\"tz\":\"Europe/London\"};\nconst c = await client.computers.get(id);\nconst result = await c.setSchedule(body);\nconsole.log(JSON.stringify(result, null, 2));"}]},"delete":{"summary":"Remove the snapshot schedule","description":"Stops automatic snapshots. Snapshots already taken are kept — but the ones this schedule took are still automatic ones, so your plan’s retention (`GET /retention`) goes on ageing them out. Take a copy by hand of anything you mean to keep past it. A computer being copied or moved answers 409; send it again once that finishes.\n\nRequires the `member` role or higher.","operationId":"deleteSchedule","tags":["Schedules"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The cleared schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schedule"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"enabled":false,"hour":0,"minute":0,"tz":"UTC"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/schedule' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.clear_schedule()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.clearSchedule();\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdSchedule","tags":["Schedules"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/retention":{"get":{"summary":"Read snapshot retention","description":"How long automatic snapshots are kept. Your plan decides this and there is no write here — which is the answer to the sentence on `PUT /computers/{id}/schedule` that says retention decides how long these live without saying what it is.\n\nAUTOMATIC CAPTURES COUNT AGAINST YOUR SNAPSHOT STORAGE ALLOWANCE. Their bytes count toward it, so they can bring an account to the point where a manual capture answers `402`, and a scheduled capture can be skipped rather than taken when it is full, or on a new account or a newly placed computer until the next complete hourly measurement reaches its host — see `PUT /computers/{id}/schedule`. Retention is what ages them out, and each plan’s allowance is sized to hold what its retention keeps.\n\nIt is a grandfather-father-son window, not an age. What survives is the NEWEST automatic snapshot in each of the last `daily` DAYS THAT HAVE ONE, the last `weekly` such ISO weeks and the last `monthly` such calendar months; every other automatic one goes. So a daily schedule on `{\"daily\": 7, \"weekly\": 4, \"monthly\": 12}` leaves you 7 days of dailies, then one a week going back a month, then one a month going back a year — at most twenty-three snapshots per computer in a steady state rather than 365, since one capture can be the newest of its day, its week and its month at once.\n\nCOUNTED IN PERIODS THAT CONTAIN A SNAPSHOT, NOT IN CALENDAR TIME, and the difference shows up the moment captures stop. Turn a schedule off for a month and come back: the last `daily` days of history are still there, because those are the last `daily` days that HAVE a snapshot and not the last `daily` days on the calendar. Nothing ages out for the passage of time alone, and the most recent automatic capture is kept whatever these numbers say.\n\nPERIOD BOUNDARIES ARE UTC — days, ISO weeks and calendar months are all cut in UTC, whatever `tz` your schedule runs in. A capture at 23:30 on a Sunday in `America/Chicago` lands on Monday UTC and so counts toward the following ISO week, which is the one place a schedule set in local time and a window measured in UTC visibly disagree.\n\nA zero turns that tier off, and all three zero means your plan grants no retained automatic history at all — which is what an account with no active subscription reads.\n\nTHAT DOES NOT MEAN YOUR EXISTING SNAPSHOTS GO. An account whose subscription lapses keeps a rolling week of its automatic snapshots — deliberately more than the all-zero answer above promises, so that an expired card is not also a lost backup. It is a grace period and not an entitlement: it is not guaranteed, it is not readable here, and it can change. If there is a capture you need to survive any window at all, take one by hand — those are never aged out.\n\nONLY SNAPSHOTS WITH `auto: true` ARE TOUCHED. One you took yourself with `POST /computers/{id}/snapshots` is yours until you delete it, whatever this says, and taking one by hand is how you keep something past the window. The state this ages out is `durable` — see `state` on Snapshot.\n\nThese numbers are your plan’s, so they change when your subscription does. They belong to the ACCOUNT but are applied PER COMPUTER: the same window is used for every computer you own, and each one keeps its own set. Two computers on `{\"daily\": 7, \"weekly\": 4, \"monthly\": 12}` keep up to twenty-three snapshots each, not twenty-three between them.\n\nRequires the `viewer` role or higher.","operationId":"getRetention","tags":["Schedules"],"x-rate-cost":1,"responses":{"200":{"description":"What your plan keeps.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Retention"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"daily":7,"weekly":0,"monthly":0}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/retention' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.snapshots.retention()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.snapshots.retention();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read snapshot retention (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetRetention","tags":["Schedules"],"responses":{"200":{"description":"What your plan keeps.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsRetention","tags":["Schedules"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/usage":{"get":{"summary":"Read usage","description":"What this account has used — running hours weighted by cores and memory, and the storage it holds — with the per-computer breakdown behind the totals. The same figures the dashboard shows, and the read to build a spend check around: a loop that launches computers is the caller that can run up a bill without noticing.\n\nThe default window is the period your plan bills on, which is what makes this comparable with an invoice — up to the 62 days this endpoint reads at once. A period longer than that (an annual plan, a long trial) is reported in `period` in full and MEASURED over the most recent 62 days of it; `from` and `to` in the response always say which window the figures cover, so compare those against `period` before reconciling. `from` and `to` override the default, and you will want them for a window that has closed — the billing period is the CURRENT one, and by the time an invoice arrives the period it covers is not.\n\nREAD `degraded` AND `unmetered` BEFORE USING THE NUMBERS. Every figure here is a sum across the hypervisors your computers are on, so a host that did not contribute does not leave a hole you could notice — it leaves a total that is quietly too small. Those two flags are how that says so, and it is why this endpoint answers 200 with a caveat rather than the 503 the listings use: one of the two shortfalls never clears by retrying.\n\nThe account, not the key: a workspace-scoped key reads the whole account’s totals, because usage is metered per account and billed per account. What it does not get is `usage.computers`, which would name machines outside its scope.\n\nOnly `from` and `to` are read. Any other query parameter is ignored rather than refused, and a bound sent twice is read from its first value.\n\nRequires the `viewer` role or higher.","operationId":"getUsage","tags":["Usage"],"x-rate-cost":25,"parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"},"description":"The start of the window, RFC 3339 **with a time zone** — `2026-08-01T00:00:00Z`. A stamp without one is refused rather than guessed at, because the zone that would be assumed is ours and not yours. Defaults to the start of the billing period, so send it together with `to` when you are asking about a period that has closed — `to` on its own is measured from the current period and refuses. When you send NEITHER bound and the billing period is longer than 62 days, the default is the most recent 62 days of it rather than a refusal, because you did not choose that window; `from` and `to` in the response say so. Records go back about 399 days, to a UTC day boundary: the refusal names the exact instant, and asking from that instant works. An older `from` is refused rather than answered with the zeroes an expired ledger would otherwise produce. A `from` at or after the present instant is a `400` too: `that period has not started yet`."},{"name":"to","in":"query","required":false,"schema":{"type":"string"},"description":"The end of it, same format. Defaults to now — always now, so omitting it always measures up to the present instant and never to the end of a period that has closed. A `to` in the future is answered as now too — the response says which instant was used. The window itself may be at most 62 days: every hypervisor replays its ledger a day at a time to answer, so a longer one THAT YOU NAMED is refused rather than quietly shortened — you chose it and can narrow it. The default window is the one exception, because you did not choose it: a billing period longer than 62 days is measured over its most recent 62 and `from`/`to` report that. Two billing periods is the pair a reconciliation compares; an older period is still readable by naming both bounds."}],"responses":{"200":{"description":"The totals for the window, and whether anything is missing from them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Usage"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"period":{"start":"2026-09-01T00:00:00.000Z","end":"2026-10-01T00:00:00.000Z","source":"calendar-month"},"from":"2026-09-01T00:00:00.000Z","to":"2026-09-16T12:00:00.000Z","usage":{"run_hours":0.5,"vcpu_hours":1,"ram_gb_hours":1,"snapshot_gb_hours":0,"snapshot_gb_months":0,"disk_gb_hours":10,"disk_gb_months":0.013689,"computers":[{"id":"vm-a1b2c3d4e5f6","name":"scratch","run_hours":0.5,"vcpu_hours":1,"ram_gb_hours":1}]},"degraded":false,"unmetered":false,"reported_through":null}},"workspace-scoped":{"summary":"Illustrative usage response for a workspace-scoped key.","value":{"period":{"start":"2026-09-01T00:00:00.000Z","end":"2026-10-01T00:00:00.000Z","source":"calendar-month"},"from":"2026-09-01T00:00:00.000Z","to":"2026-09-16T12:00:00.000Z","usage":{"run_hours":0.5,"vcpu_hours":1,"ram_gb_hours":1,"snapshot_gb_hours":0,"snapshot_gb_months":0,"disk_gb_hours":10,"disk_gb_months":0.013689},"degraded":false,"unmetered":false,"reported_through":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/usage' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.usage.read()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.usage.read();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read usage (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetUsage","tags":["Usage"],"parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"},"description":"The start of the window, RFC 3339 **with a time zone** — `2026-08-01T00:00:00Z`. A stamp without one is refused rather than guessed at, because the zone that would be assumed is ours and not yours. Defaults to the start of the billing period, so send it together with `to` when you are asking about a period that has closed — `to` on its own is measured from the current period and refuses. When you send NEITHER bound and the billing period is longer than 62 days, the default is the most recent 62 days of it rather than a refusal, because you did not choose that window; `from` and `to` in the response say so. Records go back about 399 days, to a UTC day boundary: the refusal names the exact instant, and asking from that instant works. An older `from` is refused rather than answered with the zeroes an expired ledger would otherwise produce. A `from` at or after the present instant is a `400` too: `that period has not started yet`."},{"name":"to","in":"query","required":false,"schema":{"type":"string"},"description":"The end of it, same format. Defaults to now — always now, so omitting it always measures up to the present instant and never to the end of a period that has closed. A `to` in the future is answered as now too — the response says which instant was used. The window itself may be at most 62 days: every hypervisor replays its ledger a day at a time to answer, so a longer one THAT YOU NAMED is refused rather than quietly shortened — you chose it and can narrow it. The default window is the one exception, because you did not choose it: a billing period longer than 62 days is measured over its most recent 62 and `from`/`to` report that. Two billing periods is the pair a reconciliation compares; an older period is still readable by naming both bounds."}],"responses":{"200":{"description":"The totals for the window, and whether anything is missing from them.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsUsage","tags":["Usage"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/credits":{"get":{"summary":"Read AI credits","description":"The authenticated account’s AI credit balance, live lots, current plan grant and paginated journal. Readable by every account role, including accounts with no plan or suspended accounts. Workspace-scoped keys are refused with 403 because the ledger belongs to the whole account. Amounts are integer USD micros (1,000,000 = $1), already including the funded markup. Operator notes, payment references, raw provider usage and internal request keys are never returned. Entries are newest first; follow next_before until null. Inserts made after the first page do not shift older pages. Summary values are current on each request, not pinned to the first page. This read never issues or expires credits. Unknown, duplicate, non-integer or out-of-range query parameters are refused with 400; the error names an unknown parameter, and says a repeated one is sent at most once.\n\nRequires the `viewer` role or higher.","operationId":"getCredits","tags":["Usage"],"x-rate-cost":25,"parameters":[{"name":"before","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":9007199254740991},"description":"Return entries whose id is less than this value. Omit for the newest page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Entries per page; defaults to 50, at most 100."}],"responses":{"200":{"description":"Account credit summary and one journal page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Credits"},"examples":{"success":{"summary":"An account with an unspent monthly grant.","value":{"balance_micros":5000000,"available_micros":5000000,"held_micros":0,"debt_micros":0,"frozen":false,"grant":{"period_start":"2026-10-01T00:00:00.000Z","period_end":"2026-11-01T00:00:00.000Z","granted_micros":5000000,"used_micros":0},"lots":[{"id":"lot_example","kind":"plan_grant","amount_micros":5000000,"remaining_micros":5000000,"expires_at":"2026-11-01T00:00:00.000Z","created_at":"2026-10-01T00:00:00.000Z"}],"entries":[{"id":1,"lot_id":"lot_example","kind":"credit","amount_micros":5000000,"debt_micros":0,"model":null,"run_id":null,"step":null,"created_at":"2026-10-01T00:00:00.000Z"}],"next_before":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA key confined to a workspace cannot read this account-wide credit ledger. Use an account-wide key. A suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/credits' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.credits.read()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.credits.read();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read AI credits (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetCredits","tags":["Usage"],"parameters":[{"name":"before","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":9007199254740991},"description":"Return entries whose id is less than this value. Omit for the newest page."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Entries per page; defaults to 50, at most 100."}],"responses":{"200":{"description":"Account credit summary and one journal page.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA key confined to a workspace cannot read this account-wide credit ledger. Use an account-wide key. A suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsCredits","tags":["Usage"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/webhooks":{"get":{"summary":"List webhooks","description":"Every webhook subscription on the account, with its health. No secret is ever in this list — the secret is shown once, when the subscription is created or its secret rotated.\n\nAn API key issued against a workspace sees the subscriptions confined to that workspace only.\n\nRequires the `viewer` role or higher.","operationId":"listWebhooks","tags":["Webhooks"],"x-rate-cost":1,"responses":{"200":{"description":"The subscriptions, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"whk-e1f2a3b4c5d6e7f8","url":"https://example.com/mandala-events","description":"Desktop readiness notifications","events":["computer.ready"],"computers":[],"enabled":true,"disabled_reason":null,"disabled_at":null,"last_success_at":null,"last_failure_at":null,"last_status":null,"created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:00.000Z"}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/webhooks' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.webhooks.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.webhooks.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List webhooks (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListWebhooks","tags":["Webhooks"],"responses":{"200":{"description":"The subscriptions, oldest first.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"post":{"summary":"Create a webhook","description":"Subscribe an HTTPS endpoint to this account’s events. The answer carries the signing `secret` **once**; it is not readable again, and `POST /webhooks/{id}/rotate` is how you get a new one.\n\nWHAT ARRIVES. A `POST` per event to `url`, whose body is the event object exactly as the computer’s event socket (its `events_url`) frames it — `type`, `at`, `computer`, `seq`, `cursor`, `source`, `data` — byte for byte, with nothing added and nothing wrapped around it. `cursor` is the bridge back to the stream: a job woken by `process.exited` that wants everything since can open the computer’s `events_url` with `since=` that cursor added. `computer` says which machine, and every subscription may receive events from many. Two bodies are not events and lack fields: a `gap` (below) has no `seq`, and a test delivery (`type: \"webhook.test\"`, from `POST /webhooks/{id}/test`) has neither `seq` nor `cursor`. `data` is left out of an event that has none. `WebhookEvent` in the OpenAPI document (its top-level `webhooks`) is all three as a schema.\n\nHEADERS. `content-type: application/json`, `user-agent: Mandala-Webhooks/1`, and the three [Standard Webhooks](https://www.standardwebhooks.com) headers: `webhook-id` (the delivery id, unchanged across retries), `webhook-timestamp` (Unix seconds, the time of THIS attempt) and `webhook-signature` (`v1,` and base64 of HMAC-SHA256 over `<id>.<timestamp>.<raw body>`, keyed by the secret’s bytes after `whsec_`, base64-decoded). Any Standard Webhooks verifier library checks it; verify the RAW request bytes, never a re-serialised body. Refuse a `webhook-timestamp` more than 300 seconds from your clock, and remember each `webhook-id` you accept for at least that long — together those two close every replay, and a retry of a delivery you already accepted is then recognised rather than processed twice. `mandala-subscription` names the subscription and is NOT signed: route on it, never authorise on it.\n\nACKNOWLEDGE WITH A 2xx BEFORE DOING THE WORK. An attempt is cut at 10 seconds and counted as a failure; anything else — a non-2xx, a timeout, a refused connection, a TLS error, a redirect (never followed) — is retried: eight attempts over about fourteen hours (30 s, 2 min, 10 min, 30 min, 1 h, 4 h, 8 h), then the delivery is `exhausted` and visible in `GET /webhooks/{id}/deliveries`. Retries carry the same `webhook-id` and a fresh timestamp and signature. No ordering is promised: deliveries for one subscription run four at a time and retries interleave with new events, so order by `seq` per computer if you care. At least once, never silently dropped.\n\nAN ENDPOINT THAT KEEPS FAILING IS DISABLED: when a delivery runs out of attempts and nothing has been accepted for 24 hours, `enabled` becomes `false` with `disabled_reason: \"failing\"`, pending deliveries are dropped, and `PATCH {\"enabled\": true}` starts it again. The 24 hours count from the later of the last 2xx and when the subscription was last enabled: created enabled, enabled again with `PATCH`, or turned back on by a plan. So an endpoint enabled again gets a fresh 24 hours, not the remainder of a day that began at its last 2xx.\n\nWHEN THE PLAN ENDS. An account that moves to a plan without webhooks has every enabled subscription disabled with `disabled_reason: \"plan\"`. Deliveries already queued are still made, and so is one more kind for the next 24 hours: `computer.stopped`, so you hear about the computers stopped because the plan ended (subject to the subscription’s own `events` and `computers`). Nothing else is queued. Until the account has a plan again, a new `url`, `enabled: true` and a test are `402`; reading, filters, rotating and deleting still work. Choosing a plan that includes webhooks enables them again, oldest first, as many as it allows, each starting fresh as a `PATCH` would; the rest stay disabled with `disabled_reason: \"plan\"`.\n\nGAPS. If the hypervisor holding a computer was unreachable for longer than its event journal holds, you get a `{\"type\": \"gap\", \"computer\": …}` delivery whose `data.detail` says events were lost and whose `cursor` is the computer’s own — the same frame the socket sends, and it arrives whatever `events` filters on. Reconcile with a listing rather than assume nothing happened.\n\nEvery paid plan allows ten subscriptions per account; the eleventh is a `409` naming the cap. The count is the whole account’s, so an API key confined to a workspace can meet the cap while `GET /webhooks` shows it fewer than ten.\n\nRequires the `member` role or higher.","operationId":"createWebhook","tags":["Webhooks"],"x-rate-cost":25,"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Where to POST. `https://` only, no username or password in it, at most 2048 characters — counted on the string you send, before it is parsed, so a non-ASCII character may count as two — and it must resolve to a public address — a hostname whose answers include a private, loopback, link-local or otherwise reserved address is refused, and so is a literal one. A port other than 443 is fine. The name is looked up when you send it: one that does not resolve then is a `400` (`<host> did not resolve`) even when the cause is a passing DNS failure, so a name you have not published yet cannot be registered, and the same request works once it resolves. What is stored is the URL as parsed: the host lower-cased, a default port dropped, and a `/` added to a bare origin, so compare against the stored `url` rather than what you sent.","maxLength":2048},"description":{"type":"string","description":"Free text for your listing, up to 200 characters.","maxLength":200},"events":{"type":"array","items":{"type":"string","enum":["window.opened","window.closed","window.focused","window.blurred","clipboard.changed","process.exited","computer.idle","computer.ready","computer.started","computer.stopped","computer.suspended"]},"description":"Event types to deliver, from the vocabulary on `Webhook`. Omit or send `[]` for every type. An unknown type is a `400` that lists them."},"computers":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":64,"description":"Computer ids to deliver for, up to 64, each a non-empty string of at most 64 characters. Omit or send `[]` for every computer in scope."},"enabled":{"type":"boolean","description":"Start it disabled with `false`, to enable later. Defaults to `true`."}},"required":["url"]}}}},"responses":{"201":{"description":"The subscription, with its secret — shown once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreated"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"whk-e1f2a3b4c5d6e7f8","url":"https://ci.example.com/mandala","description":"","events":["process.exited","computer.ready"],"computers":[],"enabled":true,"disabled_reason":null,"disabled_at":null,"last_success_at":null,"last_failure_at":null,"last_status":null,"created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:00.000Z","secret":"whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 16 KiB (16384 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/webhooks' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"url\":\"https://ci.example.com/mandala\",\"events\":[\"process.exited\",\"computer.ready\"]}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    body = json.loads(\"{\\\"url\\\":\\\"https://ci.example.com/mandala\\\",\\\"events\\\":[\\\"process.exited\\\",\\\"computer.ready\\\"]}\")\n    result = client.webhooks.create(body[\"url\"], events=body[\"events\"])\n    print(\"Secret returned once; store result.secret securely without logging it.\")"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst body = {\"url\":\"https://ci.example.com/mandala\",\"events\":[\"process.exited\",\"computer.ready\"]};\nconst result = await client.webhooks.create(body);\nconsole.log(\"Secret returned once; store result.secret securely without logging it.\");"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWebhooks","tags":["Webhooks"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/webhooks/{id}":{"get":{"summary":"Read a webhook","description":"One subscription, with its health: when the endpoint last accepted a delivery, when one last failed, the status of the newest attempt, and whether the platform has disabled it. Never the secret.\n\nRequires the `viewer` role or higher.","operationId":"getWebhook","tags":["Webhooks"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"200":{"description":"The subscription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"whk-e1f2a3b4c5d6e7f8","url":"https://example.com/mandala-events","description":"Desktop readiness notifications","events":["computer.ready"],"computers":[],"enabled":true,"disabled_reason":null,"disabled_at":null,"last_success_at":null,"last_failure_at":null,"last_status":null,"created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/webhooks/whk-e1f2a3b4c5d6e7f8' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"whk-e1f2a3b4c5d6e7f8\";\nconst result = await client.webhooks.get(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read a webhook (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetWebhook","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"200":{"description":"The subscription.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"patch":{"summary":"Update a webhook","description":"Change the endpoint, the description, the filters, or `enabled`. Fields you omit are left as they are; a body that names none of them is a `400`.\n\nA new `url` is checked exactly as on create. `enabled: true` clears a `failing` disable and starts fresh: the 24 hours before it can be disabled for failing again count from now, not from its last 2xx. Sent to a subscription that is already enabled, it does not restart them. `enabled: false` stops deliveries and records that you chose to. It is not a pause that holds events for later: most events while it is disabled are never queued, and a queued delivery whose next attempt falls due while it is disabled is `dropped`. Nor is it a clean cut: a retry that was not yet due, or an event that reaches the platform late, can still be delivered after you enable it again. Read the computer’s `events_url` with `since=` to reconcile, and dedupe on `webhook-id`.\n\nWhile the account’s plan includes no webhooks, a body with a `url` or with `enabled: true` is a `402` and nothing changes; the description, the filters and `enabled: false` can still be set.\n\nRequires the `member` role or higher.","operationId":"updateWebhook","tags":["Webhooks"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Where to POST. `https://` only, no username or password in it, at most 2048 characters — counted on the string you send, before it is parsed, so a non-ASCII character may count as two — and it must resolve to a public address — a hostname whose answers include a private, loopback, link-local or otherwise reserved address is refused, and so is a literal one. A port other than 443 is fine. The name is looked up when you send it: one that does not resolve then is a `400` (`<host> did not resolve`) even when the cause is a passing DNS failure, so a name you have not published yet cannot be registered, and the same request works once it resolves. What is stored is the URL as parsed: the host lower-cased, a default port dropped, and a `/` added to a bare origin, so compare against the stored `url` rather than what you sent.","maxLength":2048},"description":{"type":"string","description":"Free text for your listing, up to 200 characters.","maxLength":200},"events":{"type":"array","items":{"type":"string","enum":["window.opened","window.closed","window.focused","window.blurred","clipboard.changed","process.exited","computer.idle","computer.ready","computer.started","computer.stopped","computer.suspended"]},"description":"Event types to deliver, from the vocabulary on `Webhook`. Omit or send `[]` for every type. An unknown type is a `400` that lists them."},"computers":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":64,"description":"Computer ids to deliver for, up to 64, each a non-empty string of at most 64 characters. Omit or send `[]` for every computer in scope."},"enabled":{"type":"boolean","description":"Whether deliveries are made."}}}}}},"responses":{"200":{"description":"The subscription as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Webhook"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"whk-e1f2a3b4c5d6e7f8","url":"https://example.com/mandala-events","description":"Desktop readiness notifications","events":["computer.ready"],"computers":[],"enabled":true,"disabled_reason":null,"disabled_at":null,"last_success_at":null,"last_failure_at":null,"last_status":null,"created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:01.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 16 KiB (16384 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PATCH \"${base_url%/}\"'/webhooks/whk-e1f2a3b4c5d6e7f8' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"enabled\":true}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"whk-e1f2a3b4c5d6e7f8\"\n    body = json.loads(\"{\\\"enabled\\\":true}\")\n    result = client.webhooks.update(id, **body)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"whk-e1f2a3b4c5d6e7f8\";\nconst body = {\"enabled\":true};\nconst result = await client.webhooks.update(id, body);\nconsole.log(JSON.stringify(result, null, 2));"}]},"delete":{"summary":"Delete a webhook","description":"Removes the subscription and every delivery record it holds, pending ones included. Nothing more is sent to the endpoint.\n\nRequires the `member` role or higher.","operationId":"deleteWebhook","tags":["Webhooks"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"200":{"description":"Gone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/webhooks/whk-e1f2a3b4c5d6e7f8' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.delete(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"whk-e1f2a3b4c5d6e7f8\";\nconst result = await client.webhooks.delete(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWebhooksById","tags":["Webhooks"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/webhooks/{id}/rotate":{"post":{"summary":"Rotate a webhook secret","description":"Mints a new secret and answers it — once, like a create. The old one goes on being honoured for 24 hours: every delivery in that window carries two signatures on the one `webhook-signature` header, new first, separated by a space, and a verifier that accepts either passes throughout. Rotating again inside the window replaces the previous secret rather than keeping three.\n\nRequires the `member` role or higher.","operationId":"rotateWebhookSecret","tags":["Webhooks"],"x-rate-cost":25,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"200":{"description":"The subscription, with its new secret — shown once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreated"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"whk-e1f2a3b4c5d6e7f8","url":"https://example.com/mandala-events","description":"Desktop readiness notifications","events":["computer.ready"],"computers":[],"enabled":true,"disabled_reason":null,"disabled_at":null,"last_success_at":null,"last_failure_at":null,"last_status":null,"created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:01.000Z","secret":"whsec_Hx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQA="}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/webhooks/whk-e1f2a3b4c5d6e7f8/rotate' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.rotate(id)\n    print(\"Secret returned once; store result.secret securely without logging it.\")"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"whk-e1f2a3b4c5d6e7f8\";\nconst result = await client.webhooks.rotate(id);\nconsole.log(\"Secret returned once; store result.secret securely without logging it.\");"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWebhooksByIdRotate","tags":["Webhooks"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/webhooks/{id}/test":{"post":{"summary":"Send a test delivery","description":"Queues one signed delivery of a synthetic event — `{\"type\": \"webhook.test\", \"computer\": \"\", \"source\": \"control-plane\", \"at\": …, \"data\": {\"subscription\": …}}` — through the ordinary path, so it is signed, retried and recorded exactly as a real one. The answer is the delivery record, accepted rather than finished: read what the endpoint said back from `GET /webhooks/{id}/deliveries`. A disabled subscription is a `409`; enable it first. While the account’s plan includes no webhooks, a test is a `402`.\n\nRequires the `member` role or higher.","operationId":"testWebhook","tags":["Webhooks"],"x-rate-cost":25,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"202":{"description":"The queued delivery.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"whd-f1a2b3c4d5e6f7a8","event_type":"webhook.test","computer":"","cursor":"test:whd-f1a2b3c4d5e6f7a8","state":"pending","attempts":0,"next_at":"2026-09-16T12:00:00.000Z","attempted_at":null,"last_status":null,"last_error":null,"delivered_at":null,"created_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/webhooks/whk-e1f2a3b4c5d6e7f8/test' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.test(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"whk-e1f2a3b4c5d6e7f8\";\nconst result = await client.webhooks.test(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWebhooksByIdTest","tags":["Webhooks"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/webhooks/{id}/deliveries":{"get":{"summary":"List deliveries","description":"The newest hundred deliveries to this subscription, newest first, each with its state, its attempt count and the status or one-line error of its newest attempt. Finished deliveries are kept until seven days after they were queued; pending ones are kept until they finish. This is where an `exhausted` delivery shows up — nothing is dropped silently.\n\nRequires the `viewer` role or higher.","operationId":"listWebhookDeliveries","tags":["Webhooks"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"200":{"description":"The deliveries, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"whd-f1a2b3c4d5e6f7a8","event_type":"webhook.test","computer":"","cursor":"test:whd-f1a2b3c4d5e6f7a8","state":"delivered","attempts":1,"next_at":null,"attempted_at":"2026-09-16T12:00:01.000Z","last_status":204,"last_error":null,"delivered_at":"2026-09-16T12:00:01.000Z","created_at":"2026-09-16T12:00:00.000Z"}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/webhooks/whk-e1f2a3b4c5d6e7f8/deliveries' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.deliveries(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"whk-e1f2a3b4c5d6e7f8\";\nconst result = await client.webhooks.deliveries(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List deliveries (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListWebhookDeliveries","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"200":{"description":"The deliveries, newest first.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWebhooksByIdDeliveries","tags":["Webhooks"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The webhook id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/ssh-keys":{"get":{"summary":"List your SSH keys","description":"The SSH public keys registered to **you** — not to the account. A key identifies one person, so this is the same list whichever account your key or session is acting on. A key added from the dashboard reaches the computers of every account where you are an owner or member. A key added through an API key or a connected app reaches only the account that credential acts on, and is removed when the credential is revoked. Each key’s `reach` says which of those it is, from the account this request acts on, without naming any other. Viewers cannot connect.\n\nHOW TO CONNECT. Switch SSH on for a computer (`PUT /computers/{id}/ssh`), then `ssh -J mandala@ssh.mandala.computer:2222 user@<computer id or name>`. The jump host checks the key you offer against this list and your role on the computer’s account; the computer’s own sshd then checks it again. A name that matches computers on more than one of your accounts is refused — use the id.\n\nConnecting to a suspended computer resumes it — a start like any other, charged and refusable by your plan. A stopped computer refuses the connection; start it first.\n\nRequires the `viewer` role or higher.","operationId":"listSSHKeys","tags":["SSH"],"x-rate-cost":1,"responses":{"200":{"description":"Your keys, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SSHKey"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"sshk-a1b2c3d4e5f60718","name":"laptop","public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMxlN5MDRT9cXdHi871o7Ty3dKfNLt8mNmjSWtwv6DTw","fingerprint":"SHA256:+GiGZKWHEUZeM+kzujljZNiEU86lD9XvR2QBUw90/r8","key_type":"ssh-ed25519","created_at":"2026-09-16T12:00:00.000Z","last_used_at":null,"reach":"everywhere"}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/ssh-keys' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.ssh_keys.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.sshKeys.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List your SSH keys (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListSSHKeys","tags":["SSH"],"responses":{"200":{"description":"Your keys, oldest first.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"post":{"summary":"Add an SSH key","description":"Registers a public key to you. Accepted: Ed25519, ECDSA (P-256, P-384, P-521), the FIDO security-key forms `sk-ssh-ed25519@openssh.com` and `sk-ecdsa-sha2-nistp256@openssh.com`, and RSA of at least 3072 bits. Refused with a `400`: DSA, shorter RSA, certificates, a line with `authorized_keys` options in front of the key, and anything that is not one key on one line.\n\nA key can belong to one person only; registering one that is already registered is a `409`. Each person may hold eight; the ninth is a `409`.\n\nThe key is bound to the account this API key (or connected app) acts on, and to the credential itself: it is written into every computer with SSH on, on that account only, within moments, and it is removed when the credential is revoked. A key added from the dashboard instead reaches every account where you are an owner or member. The answer does not wait for the computers.\n\nAn API key confined to a workspace cannot add or remove SSH keys (`403`): an SSH key reaches further than that workspace.\n\nRequires the `member` role or higher.","operationId":"createSSHKey","tags":["SSH"],"x-rate-cost":5,"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"public_key":{"type":"string","description":"One line from a `.pub` file: `<type> <base64> [comment]`, at most 8192 characters. The comment is dropped. The file read whole is fine: one trailing line ending (`\\n` or `\\r\\n`) is ignored and not counted, but a second line is a `400`.","maxLength":8194},"name":{"type":"string","description":"A label, up to 60 characters after trimming. Left out or `null`, it is the comment on the line, with any control characters removed and cut to 60 characters, or `key` if there is none; an empty or blank name is `key` too."}},"required":["public_key"]}}}},"responses":{"201":{"description":"The key as registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SSHKey"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"sshk-a1b2c3d4e5f60718","name":"laptop","public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMxlN5MDRT9cXdHi871o7Ty3dKfNLt8mNmjSWtwv6DTw","fingerprint":"SHA256:+GiGZKWHEUZeM+kzujljZNiEU86lD9XvR2QBUw90/r8","key_type":"ssh-ed25519","created_at":"2026-09-16T12:00:00.000Z","last_used_at":null,"reach":"everywhere"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso, with no `reason` and checked first: a holder who has not verified their email address (`Verify your email before adding an SSH key.`); verify it in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nEither refusal here carries no `reason`, and neither clears by waiting: the key is already registered (to you or to someone else), or you already hold eight keys. Remove a key first, or register a different one; sending the same request again is refused the same way.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 16 KiB (16384 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/ssh-keys' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"public_key\":\"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMxlN5MDRT9cXdHi871o7Ty3dKfNLt8mNmjSWtwv6DTw you@laptop\",\"name\":\"laptop\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    body = json.loads(\"{\\\"public_key\\\":\\\"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMxlN5MDRT9cXdHi871o7Ty3dKfNLt8mNmjSWtwv6DTw you@laptop\\\",\\\"name\\\":\\\"laptop\\\"}\")\n    result = client.ssh_keys.add(body[\"public_key\"], name=body[\"name\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst body = {\"public_key\":\"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMxlN5MDRT9cXdHi871o7Ty3dKfNLt8mNmjSWtwv6DTw you@laptop\",\"name\":\"laptop\"};\nconst result = await client.sshKeys.add({publicKey: body.public_key, name: body.name});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSshKeys","tags":["SSH"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/ssh-keys/{id}":{"delete":{"summary":"Remove an SSH key","description":"Removes one of your keys. New connections with it are refused at once. Within moments it is removed from the computers it was written into, and any SSH session opened with it is closed as each computer receives the new key list. Sessions opened with your other keys are not affected. Not available to a workspace-scoped API key (`403`).\n\nAn API key or connected app removes only a key bound to the account it acts on, one whose `reach` is `this_account`. Any other key, whether its `reach` is `everywhere` (added from the dashboard, or through the API before keys were bound to an account) or `another_account`, is a `404`, as for a key that does not exist. Remove it from the dashboard; a key whose `reach` is `another_account` can also be removed with an API key on the account it was added through.\n\nRequires the `member` role or higher.","operationId":"deleteSSHKey","tags":["SSH"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The SSH key id."}],"responses":{"200":{"description":"Gone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/ssh-keys/sshk-a1b2c3d4e5f60718' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"sshk-a1b2c3d4e5f60718\"\n    result = client.ssh_keys.remove(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"sshk-a1b2c3d4e5f60718\";\nconst result = await client.sshKeys.remove(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSshKeysById","tags":["SSH"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The SSH key id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/ssh":{"get":{"summary":"Read a computer’s SSH setting","description":"Whether SSH is on for this computer, whether it can work here at all, and whether the computer’s hypervisor has the current setting yet. Any role may read it.\n\nRequires the `viewer` role or higher.","operationId":"getComputerSSH","tags":["SSH"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The setting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputerSSH"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"computer":"vm-a1b2c3d4e5f6","enabled":false,"available":null,"pending":false,"key_count":0,"keys_pushed":0,"error":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/ssh' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.ssh_access()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst c = await client.computers.get(id);\nconst result = await c.sshAccess();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read a computer’s SSH setting (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetComputerSSH","tags":["SSH"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"The setting.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"put":{"summary":"Switch SSH on or off","description":"Turns SSH on or off for this computer. On, the computer runs an SSH server reachable only through the platform’s jump host, and accepts the keys of every owner and member of the account — not those of a person whose invitation is unaccepted, who is suspended or whose seat is suspended, and none while the account is suspended (see `key_count`); off, the server stops, the key file is removed and open SSH sessions are closed. No restart either way.\n\nA computer made from a template image that predates SSH cannot run it: the setting is stored and the answer says `available: false`. Create a new computer from the current template instead.\n\nIf the computer’s hypervisor cannot be reached the setting is still stored, the answer says `pending: true`, and it is delivered when the hypervisor is back — at the latest when somebody next connects or reads this setting.\n\nConnecting to a suspended computer resumes it — a start like any other, charged and refusable by your plan. A stopped computer refuses the connection; start it first.\n\nRequires the `member` role or higher.","operationId":"setComputerSSH","tags":["SSH"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean","description":"`true` to switch SSH on, `false` to switch it off."}},"required":["enabled"]}}}},"responses":{"200":{"description":"The setting as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputerSSH"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"computer":"vm-a1b2c3d4e5f6","enabled":true,"available":true,"pending":false,"key_count":1,"keys_pushed":1,"error":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 16 KiB (16384 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PUT \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/ssh' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"enabled\":true}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"enabled\\\":true}\")\n    c = client.computers.get(id)\n    result = c.set_ssh_access(body[\"enabled\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"enabled\":true};\nconst c = await client.computers.get(id);\nconst result = await c.setSshAccess(body.enabled);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdSsh","tags":["SSH"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/public-urls":{"get":{"summary":"List a computer’s public URLs","description":"A public URL serves a port inside the computer at `https://<label>.<base_domain>` (e.g. `mandala.dev`), on port 443 with no port in the address. The app must listen on `0.0.0.0:<port>`, not `127.0.0.1`.\n\n`enabled` is `false`, with a `reason`, where public URLs are off here or the account’s plan includes none; the computer’s URLs are listed either way, so you can see and delete what you made. If the plan drops below the number a computer has, the oldest within the new limit keep answering and the rest answer 404 until the plan allows them again.\n\nWhile a change of this computer’s workspace is unsettled, this is a `409` with code `WORKSPACE_MOVING`: `reason: \"contention\"` while the move is being made (try again once it has finished), or `reason: \"unsupported\"` when its outcome was lost (check the workspace change in the dashboard). How a URL answers, and who may open a private one, is set out under `POST /computers/{id}/public-urls`.\n\nRequires the `viewer` role or higher.","operationId":"listPublicUrls","tags":["Public URLs"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"Whether they can answer, the limit, and the URLs, oldest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUrlList"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"enabled":true,"base_domain":"mandala.dev","limit":2,"public_urls":[{"id":"pu-0123456789abcdef","url":"https://cool-thing.mandala.dev","hostname":"cool-thing.mandala.dev","label":"cool-thing","port":3000,"access":"public","disabled":false,"created_at":"2026-10-12T18:00:00.000Z","updated_at":"2026-10-12T18:00:00.000Z"}]}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/public-urls' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    result = client.public_urls.list(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst result = await client.publicUrls.list(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List a computer’s public URLs (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListPublicUrls","tags":["Public URLs"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"200":{"description":"Whether they can answer, the limit, and the URLs, oldest first.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"post":{"summary":"Make a public URL for a port on the computer","description":"Serve `port` on `https://<label>.<base_domain>`. Without a `label`, a name like `brave-otter-3f2a` is picked. The body is at most 4096 bytes. The errors this route adds are `{ error, code }`, except four that carry `error` alone: a body over that size (`413`), a body that is not JSON or not a JSON object (`400`), a computer that is not found (`404`), and the `503` at the end of this list.\n\n- `400` `INVALID_BODY`: a field other than `port`, `label` and `access`, named in the error. Nothing is made.\n- `400` `INVALID_PORT`: not an integer from 1 to 65535, or 22, which belongs to the computer.\n- `400` `INVALID_LABEL`: 3 to 40 letters, digits and hyphens, starting and ending with a letter or digit, not all digits, no hyphens in the third and fourth places (`ab--`), not shaped like a computer id, and not a name that reads as the platform’s or another company’s own address: a reserved word such as `www` or `login`, numbered, spelled with look-alikes or split by hyphens (`www2`, `l0gin`, `sign-in`); `mandala` as a part, or run together with reserved words (`mandala-login`, `mandalalogin`); a well-known brand among its parts, whole or split (`paypal-refunds`, `pay-pal-refunds`), or for the most distinctive brands anywhere in a part (`paypalrefunds`); or nothing but reserved words and numbers (`secure-login`, `securelogin`). Surrounding spaces are trimmed and it is lowercased.\n- `400` `INVALID_ACCESS`: `access` is `public` (the default) or `private`.\n- `402` `PLAN_REQUIRED`: the account’s plan includes no public URLs.\n- `403` `PUBLIC_URLS_BLOCKED`: the platform has turned public URLs off for this computer or account.\n- `404` `PUBLIC_URLS_DISABLED`: public URLs are not enabled here.\n- `409` `LABEL_TAKEN` or `PUBLIC_URL_LIMIT`: the name is taken, or the computer has as many as its plan allows (Solo 2, Studio 5, Swarm 10). A name the platform has blocked reads as taken, and so does one another account has ever held: a name is never given to a second account.\n- `409` `WORKSPACE_MOVING`: a change of this computer’s workspace is unsettled. `reason` is `contention` while the move is being made, so try again in a moment, and `unsupported` when its outcome was lost, so check the workspace change in the dashboard.\n- `429` `RATE_LIMITED`: this account has made 25 in the last 24 hours, deleted ones included. `Retry-After` says when to try again.\n- `503`, with no `code`: no free generated name was found. Try again, or give a `label`.\n\nWHO MAY OPEN IT. A `public` URL answers anyone with the link. A `private` one answers two credentials and nothing else (`401`): an API key of this account sent as `Authorization: Bearer`, where a key confined to a workspace opens only the URLs of that workspace’s computers; or a member of the account whose browser is signed in to that one URL from the dashboard, whose Open button sends it through a one-time sign-in link good for a minute. That sign-in lasts at most 12 hours and ends with the dashboard session it came from, and it admits requests from the URL’s own pages and a top-level visit to it, never a fetch, script, frame or form post from another site. A browser opening a private URL with neither gets a page saying how to open it, still a `401`; any other request without one gets an empty `401`. While a change of the computer’s workspace is unsettled, a private URL answers `503` to everyone: which workspace’s keys may open it is not settled yet. Neither credential reaches the app, and neither does a Mandala API key or MCP token sent to a public URL. Any other `Authorization` header is the app’s own and passes through. Cookies the app sets stay on its own host name.\n\nA NEW ACCOUNT’S URLS WARN A BROWSER FIRST. For an account created in the last 14 days, a browser opening one of its URLs, public or private, is sent to a warning page at `/__mandala/warning` first; Continue takes it on, and that browser is not warned about that URL again for 30 days. A browser navigation is told by fetch metadata — `Sec-Fetch-Mode: navigate`, or a `Sec-Fetch-Dest` of `document` or a frame — or, from a browser that sends no fetch metadata, by an `Accept` naming `text/html` with a `User-Agent` beginning `Mozilla/`. A programmatic client that sends none of those never sees the page. An agent driving a real browser skips it by sending the request header `mandala-skip-warning`, with any value.\n\nWHAT THE APP CANNOT HAVE. `/__mandala` and every path under `/__mandala/` belong to the platform and never reach the app. A service worker cannot be registered: a request for a worker script (`Service-Worker: script`) is answered `403`. The app cannot set a cookie named `__Host-mandala_*`, which is dropped, and never sees the platform’s own. Every answer is marked `X-Robots-Tag: noindex, nofollow` and is never cached at the edge; a private URL’s answers are also sent `Cache-Control: no-store`, without `ETag` or `Last-Modified`.\n\nHOW IT ANSWERS. A URL serves a running computer. A stopped or suspended computer’s URLs answer `503`, and traffic never starts or resumes it; traffic does count as use, so it keeps a running computer from being suspended for idleness. Nothing listening on the port is a `503` with `Retry-After`, and a browser opening a page gets one saying nothing is listening yet. A request body is at most 100 MiB (`413`), one computer takes at most 256 requests through its URLs at once (`503` past that), the app has 100 seconds to start answering (`504`), and a request that moves no bytes either way for five minutes, a WebSocket included, is ended.\n\nRequires the `member` role or higher.","operationId":"createPublicUrl","tags":["Public URLs"],"x-rate-cost":25,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"port":{"type":"integer","description":"The port inside the computer to serve, 1 to 65535 except 22.","minimum":1,"maximum":65535},"label":{"type":"string","description":"The name before the domain, 3 to 40 characters once trimmed; see `INVALID_LABEL`. Omit to have one picked."},"access":{"type":"string","enum":["public","private"],"default":"public","description":"`public` (the default) or `private`."}},"required":["port"],"additionalProperties":false}}}},"responses":{"201":{"description":"The URL as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUrl"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"pu-0123456789abcdef","url":"https://cool-thing.mandala.dev","hostname":"cool-thing.mandala.dev","label":"cool-thing","port":3000,"access":"public","disabled":false,"created_at":"2026-10-12T18:00:00.000Z","updated_at":"2026-10-12T18:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/public-urls' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"port\":3000,\"label\":\"cool-thing\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"port\\\":3000,\\\"label\\\":\\\"cool-thing\\\"}\")\n    result = client.public_urls.create(id, body[\"port\"], label=body[\"label\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst body = {\"port\":3000,\"label\":\"cool-thing\"};\nconst result = await client.publicUrls.create(id, body);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdPublicUrls","tags":["Public URLs"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/computers/{id}/public-urls/{urlId}":{"patch":{"summary":"Point a public URL at another port","description":"Change the port the URL serves, who may open it, or both. The label is the URL and does not change. The body is at most 4096 bytes (`413`). The errors this route adds are `{ error, code }`, except that a body that is not JSON or not a JSON object, the `413`, and a computer or URL that is not found carry `error` alone:\n\n- `400` `INVALID_BODY`: the body names neither `port` nor `access`, or has a field other than `port` and `access` (named in the error, and nothing changes).\n- `400` `INVALID_PORT` or `INVALID_ACCESS`: as on create.\n- `404` `PUBLIC_URLS_DISABLED`: public URLs are not enabled here.\n- `409` `WORKSPACE_MOVING`: a change of this computer’s workspace is unsettled, with `reason` as on create.\n\nRequires the `member` role or higher.","operationId":"updatePublicUrl","tags":["Public URLs"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"urlId","in":"path","required":true,"schema":{"type":"string"},"description":"A public URL id from `GET /computers/{id}/public-urls` (`pu-` followed by 16 hex characters)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"port":{"type":"integer","description":"The port inside the computer to serve, 1 to 65535 except 22.","minimum":1,"maximum":65535},"access":{"type":"string","enum":["public","private"],"description":"`public` or `private`."}},"additionalProperties":false}}}},"responses":{"200":{"description":"The URL as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUrl"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"pu-0123456789abcdef","url":"https://cool-thing.mandala.dev","hostname":"cool-thing.mandala.dev","label":"cool-thing","port":8000,"access":"public","disabled":false,"created_at":"2026-10-12T18:00:00.000Z","updated_at":"2026-10-12T18:05:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PATCH \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/public-urls/pu-0123456789abcdef' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"port\":8000}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    urlId = \"pu-0123456789abcdef\"\n    body = json.loads(\"{\\\"port\\\":8000}\")\n    result = client.public_urls.update(id, urlId, port=body[\"port\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst urlId = \"pu-0123456789abcdef\";\nconst body = {\"port\":8000};\nconst result = await client.publicUrls.update(id, urlId, body);\nconsole.log(JSON.stringify(result, null, 2));"}]},"delete":{"summary":"Remove a public URL","description":"The URL stops answering at once. Its label stays this account’s: no other account can ever take it, and this one may use it again unless the platform has blocked it. The app inside the computer keeps running. Allowed at any time, an unsettled workspace change included, and with public URLs switched off here. A computer or URL that is not found is a `404`.\n\nRequires the `member` role or higher.","operationId":"deletePublicUrl","tags":["Public URLs"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"urlId","in":"path","required":true,"schema":{"type":"string"},"description":"A public URL id from `GET /computers/{id}/public-urls` (`pu-` followed by 16 hex characters)."}],"responses":{"200":{"description":"Gone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/computers/vm-a1b2c3d4e5f6/public-urls/pu-0123456789abcdef' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"vm-a1b2c3d4e5f6\"\n    urlId = \"pu-0123456789abcdef\"\n    result = client.public_urls.delete(id, urlId)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst urlId = \"pu-0123456789abcdef\";\nconst result = await client.publicUrls.delete(id, urlId);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsComputersByIdPublicUrlsByUrlId","tags":["Public URLs"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer id."},{"name":"urlId","in":"path","required":true,"schema":{"type":"string"},"description":"A public URL id from `GET /computers/{id}/public-urls` (`pu-` followed by 16 hex characters)."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/secrets":{"get":{"summary":"List secrets","description":"The secrets in one scope: the account-wide ones by default, or one workspace’s with `workspace_id`. Names, ids and revisions only — no route ever returns a value.\n\nOwners and members may read; viewers cannot, not even the names. An API key confined to a workspace lists that workspace’s secrets, and naming any other scope is a `403`. From an account-wide key, a `workspace_id` that names no workspace of this account (an empty one included) is a `400`, `No such workspace on this account.`\n\nRequires the `member` role or higher.","operationId":"listSecrets","tags":["Secrets"],"x-rate-cost":1,"parameters":[{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"The workspace to list. Leave it out for the account-wide secrets."}],"responses":{"200":{"description":"The secrets in that scope, and the limits.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretList"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"secrets":[{"id":"csec-0123456789abcdef","name":"OPENAI_API_KEY","workspace_id":null,"revision_id":"csr-0123456789abcdef01234567","created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:00.000Z","last_used_at":null}],"delivery":true,"limits":{"name_max_chars":60,"value_max_bytes":4096,"active_per_account":100,"created_per_account":1000}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso `Secrets are not enabled on this control plane.` when this deployment has secrets switched off. That one does not clear by retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/secrets' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.secrets.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.secrets.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List secrets (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headListSecrets","tags":["Secrets"],"parameters":[{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"The workspace to list. Leave it out for the account-wide secrets."}],"responses":{"200":{"description":"The secrets in that scope, and the limits.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso `Secrets are not enabled on this control plane.` when this deployment has secrets switched off. That one does not clear by retrying.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"post":{"summary":"Create a secret","description":"Stores a value under a name, encrypted, for binding to computers with `PUT /computers/{id}/secrets`. The answer is the secret without its value; the value cannot be read back by any route.\n\nOwners only. A name is unique within its scope: a taken one is a `409` with `reason: \"name_taken\"`, and sending the same request again gets the same answer.\n\nTWO LIMITS, both answered `400`: the account holds at most 100 secrets at once, and deleting one frees a place. A workspace holding active secrets cannot be deleted (see `DELETE /workspaces/{id}`). Secrets orphaned by older workspace deletions remain unreachable and still count; this safeguard does not repair them. The account may create at most 1000 over its lifetime, deleted ones included, which deleting does not free — once that is reached, only support can raise it. `GET /secrets` reports both in `limits`.\n\nWithout `workspace_id` the secret is account-wide, and a computer in any workspace may be bound to it. An API key confined to a workspace creates in that workspace, and naming any other scope is a `403`. From an account-wide key, a `workspace_id` that names no workspace of this account is a `400`, `No such workspace on this account.`\n\nRequires the `owner` role or higher.","operationId":"createSecret","tags":["Secrets"],"x-rate-cost":1,"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Up to 60 characters, no control characters. Leading and trailing spaces are trimmed, and the trimmed name is stored. Unique within the scope, ignoring ASCII case: a name that differs from a taken one only in ASCII case (`Token` and `token`) collides, a `409` with `reason: \"name_taken\"`.","minLength":1},"value":{"type":"string","description":"The value, as text: 1 to 4096 bytes of UTF-8."},"workspace_id":{"type":["string","null"],"description":"The workspace it belongs to. Leave it out, or send `null`, for account-wide."}},"required":["name","value"]}}}},"responses":{"201":{"description":"The secret, without its value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"csec-0123456789abcdef","name":"OPENAI_API_KEY","workspace_id":null,"revision_id":"csr-0123456789abcdef01234567","created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:00.000Z","last_used_at":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 32 KiB (32768 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso `Secrets are not enabled on this control plane.` when this deployment has secrets switched off. That one does not clear by retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/secrets' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"OPENAI_API_KEY\",\"value\":\"sk-…\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    body = json.loads(\"{\\\"name\\\":\\\"OPENAI_API_KEY\\\",\\\"value\\\":\\\"sk-…\\\"}\")\n    result = client.secrets.create(body[\"name\"], body[\"value\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst body = {\"name\":\"OPENAI_API_KEY\",\"value\":\"sk-…\"};\nconst result = await client.secrets.create({name: body.name, value: body.value});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSecrets","tags":["Secrets"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/secrets/{id}":{"get":{"summary":"Read a secret","description":"One secret’s name, scope and current `revision_id` — which a replace or delete needs — without its value. A secret in a workspace is found only with that `workspace_id`; for any other it is a `404`, a `workspace_id` that names no workspace of this account included. An API key confined to a workspace that names any other scope is refused with `403` instead; an account-wide key gets the `404`.\n\nRequires the `member` role or higher.","operationId":"getSecret","tags":["Secrets"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The secret id."},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"The workspace the secret belongs to. Leave it out for an account-wide one."}],"responses":{"200":{"description":"The secret, without its value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"csec-0123456789abcdef","name":"OPENAI_API_KEY","workspace_id":null,"revision_id":"csr-0123456789abcdef01234567","created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-16T12:00:00.000Z","last_used_at":null}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso `Secrets are not enabled on this control plane.` when this deployment has secrets switched off. That one does not clear by retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/secrets/csec-0123456789abcdef' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"csec-0123456789abcdef\"\n    result = client.secrets.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"csec-0123456789abcdef\";\nconst result = await client.secrets.get(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Read a secret (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `member` role or higher.","operationId":"headGetSecret","tags":["Secrets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The secret id."},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"The workspace the secret belongs to. Leave it out for an account-wide one."}],"responses":{"200":{"description":"The secret, without its value.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso `Secrets are not enabled on this control plane.` when this deployment has secrets switched off. That one does not clear by retrying.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}}},"put":{"summary":"Replace a secret’s value","description":"Replaces the value and moves `revision_id`. Send the `revision_id` you read: if it has moved since, the answer is `409` with `reason: \"stale_revision\"` and nothing changed. Sending the same request again gets the same answer: read the secret again and retry with its current `revision_id`.\n\nA computer bound to it as an environment variable gets the new value in its desktop session at its next start or restart; on an image that supports it, a running one is also sent the new value for new shells and for every `exec`, plain or with `session: \"desktop\"`, so the next one sees it, while programs already running keep the value they started with. One bound to it as a FILE is also sent the new value while it runs. Both are sent asynchronously and on a best-effort basis: the answer does not wait for that, and does not say whether it landed. Unless the update was confirmed — not if its host was unreachable, or refused it, or took it and never answered — a running computer may still hold the old value until its next start or restart, which always delivers the latest.\n\nA secret a computer’s `egress_proxy.credentials_secret_id` names must keep a `user:password` value (a colon, no control characters, one trailing newline ignored): any other is refused with `400` and nothing changed. Owners only.\n\nA secret in a workspace is found only with that `workspace_id`; for any other it is a `404`, a `workspace_id` that names no workspace of this account included. An API key confined to a workspace that names any other scope, `null` included, is refused with `403` instead; an account-wide key gets the `404`.\n\nRequires the `owner` role or higher.","operationId":"replaceSecret","tags":["Secrets"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The secret id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"string","description":"The new value: 1 to 4096 bytes of UTF-8."},"revision_id":{"type":"string","description":"The `revision_id` a read of this secret answered."},"workspace_id":{"type":["string","null"],"description":"The workspace the secret belongs to. Leave it out for an account-wide one."}},"required":["value","revision_id"]}}}},"responses":{"200":{"description":"The secret at its new revision, without its value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Secret"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"csec-0123456789abcdef","name":"OPENAI_API_KEY","workspace_id":null,"revision_id":"csr-89abcdef0123456789abcdef","created_at":"2026-09-16T12:00:00.000Z","updated_at":"2026-09-23T09:30:00.000Z","last_used_at":"2026-09-20T08:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 32 KiB (32768 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso `Secrets are not enabled on this control plane.` when this deployment has secrets switched off. That one does not clear by retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PUT \"${base_url%/}\"'/secrets/csec-0123456789abcdef' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"value\":\"sk-…\",\"revision_id\":\"csr-0123456789abcdef01234567\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"csec-0123456789abcdef\"\n    body = json.loads(\"{\\\"value\\\":\\\"sk-…\\\",\\\"revision_id\\\":\\\"csr-0123456789abcdef01234567\\\"}\")\n    result = client.secrets.replace(id, body[\"value\"], revision_id=body[\"revision_id\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"csec-0123456789abcdef\";\nconst body = {\"value\":\"sk-…\",\"revision_id\":\"csr-0123456789abcdef01234567\"};\nconst result = await client.secrets.replace(id, {value: body.value, revisionId: body.revision_id});\nconsole.log(JSON.stringify(result, null, 2));"}]},"delete":{"summary":"Delete a secret","description":"Deletes it. `revision_id` is required, and a stale one is a `409` with `reason: \"stale_revision\"` and nothing deleted; sending the same request again gets the same answer. Owners only.\n\nDeleting does not recall a value already delivered, and it is not refused while computers are bound to the secret. A running computer bound to it keeps the value it holds. One window remains: if a replace of this secret was still being sent to a computer bound to it as a file when the delete committed, that value may still arrive — a send not yet dispatched is abandoned, one already on its way is not.\n\nNothing more is delivered after the delete, so a computer still bound to it cannot be given its secrets again. Its next start, and a disk snapshot restored onto it, are refused with `409` and `reason: \"unsupported\"` until the binding is removed; a restart, or a reboot inside the guest, stops it rather than bring it up without them; and a suspended one cannot resume its saved session. Remove the binding with `PUT /computers/{id}/secrets` — before deleting, or on the stopped computer afterwards — to start it again.\n\nAN EGRESS PROXY THAT SIGNS IN WITH IT IS SHUT OFF. Every computer whose `egress_proxy.credentials_secret_id` names this secret has its proxy credentials revoked on this control plane before the answer is sent, so nothing is sent to its host for it again. Its host is then told to drop the value it holds, without the answer waiting for that: until the host has, new connections can still sign in with it. Once it has, every connection that computer opens is refused rather than sent directly (`egress_proxy_pending`), until its `egress_proxy` is saved naming a secret that exists. A host that could not be told is told again by the platform’s next reconcile.\n\nA secret in a workspace is found only with that `workspace_id`; for any other it is a `404`, a `workspace_id` that names no workspace of this account included. An API key confined to a workspace that names any other scope is refused with `403` instead; an account-wide key gets the `404`.\n\nRequires the `owner` role or higher.","operationId":"deleteSecret","tags":["Secrets"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The secret id."},{"name":"revision_id","in":"query","required":true,"schema":{"type":"string"},"description":"The `revision_id` a read of this secret answered. Missing or malformed is a `400`."},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"string"},"description":"The workspace the secret belongs to. Leave it out for an account-wide one."}],"responses":{"200":{"description":"Gone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nAlso `Secrets are not enabled on this control plane.` when this deployment has secrets switched off. That one does not clear by retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/secrets/csec-0123456789abcdef?revision_id=csr-0123456789abcdef01234567' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"csec-0123456789abcdef\"\n    query = {\"revision_id\":\"csr-0123456789abcdef01234567\"}\n    result = client.secrets.delete(id, revision_id=query[\"revision_id\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"csec-0123456789abcdef\";\nconst query = {\"revision_id\":\"csr-0123456789abcdef01234567\"};\nconst result = await client.secrets.delete(id, {revisionId: query.revision_id});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsSecretsById","tags":["Secrets"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The secret id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/workspaces":{"get":{"summary":"List workspaces","description":"The account’s workspaces, oldest first. A workspace partitions the account’s computers: an API key issued against one reaches that workspace’s computers only, and a computer’s `workspace_id` names the workspace it is in.\n\nAn API key confined to a workspace lists that one workspace and no other. An account-wide key sees them all, whatever its holder’s role.\n\nA member or owner can move a computer with an account-wide credential using `PUT /computers/{id}/workspace`, or in the dashboard’s Settings → General → Workspace, to another workspace or to none. The computer must be stopped with no saved session, and have no snapshots, no secret bindings and no proxy credentials. Once it has moved, API keys confined to its old workspace get `404` for it and keys confined to the new one can reach it; its events, webhooks included, follow it to the new workspace; and its desktop and share credentials are replaced, so links from before stop working.\n\nRequires the `viewer` role or higher.","operationId":"listWorkspaces","tags":["Workspaces"],"x-rate-cost":1,"responses":{"200":{"description":"The workspaces, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Workspace"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"wsp-a1b2c3d4e5f6","name":"ci","created_at":"2026-09-16T12:00:00.000Z"}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/workspaces' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.workspaces.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.workspaces.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List workspaces (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListWorkspaces","tags":["Workspaces"],"responses":{"200":{"description":"The workspaces, oldest first.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"post":{"summary":"Create a workspace","description":"Makes a workspace on the account. Owners only, with an account-wide API key: a key confined to a workspace is refused (`403`), because it could otherwise make itself somewhere new to stand.\n\nThe name is trimmed, must be 1 to 40 characters with no ASCII control characters (U+0000 to U+001F and U+007F), and must not be one the account already uses; an account holds at most 500 workspaces. Each of those is a `400` with a sentence, as is any field but `name`.\n\nRequires the `owner` role or higher.","operationId":"createWorkspace","tags":["Workspaces"],"x-rate-cost":1,"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Its name: 1 to 40 characters after trimming, unique within the account.","minLength":1}},"required":["name"]}}}},"responses":{"201":{"description":"The new workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"wsp-a1b2c3d4e5f6","name":"customer-acme","created_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 4 KiB (4096 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/workspaces' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"customer-acme\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    body = json.loads(\"{\\\"name\\\":\\\"customer-acme\\\"}\")\n    result = client.workspaces.create(body[\"name\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst body = {\"name\":\"customer-acme\"};\nconst result = await client.workspaces.create({name: body.name});\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWorkspaces","tags":["Workspaces"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/workspaces/{id}":{"get":{"summary":"Get a workspace","description":"One workspace. An id this credential cannot see is a `404`, the same as one that does not exist: another account’s, and — for an API key confined to a workspace — any workspace but its own.\n\nA computer can carry the `workspace_id` of a workspace that has since been deleted, if the workspace was deleted while it still held computers, before that was refused; if a create or a snapshot clone into it was under way when it was deleted; or if it is a clone (`POST /computers/{id}/clone`) of such a computer, which keeps its source’s `workspace_id`. That id answers `404` here. One thing about such a computer is handled: its egress credentials resolve account-wide only, so an `egress_proxy` naming one of that workspace’s own secrets is held closed (`egress_proxy_pending` `true`) until it is saved again naming an account-wide secret. Such a computer can be moved to a live workspace, or to none, with `PUT /computers/{id}/workspace` or from the dashboard once its secret bindings and proxy credentials are removed and it meets the other conditions under `GET /workspaces`.\n\nRequires the `viewer` role or higher.","operationId":"getWorkspace","tags":["Workspaces"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The workspace id."}],"responses":{"200":{"description":"The workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"wsp-a1b2c3d4e5f6","name":"ci","created_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/workspaces/wsp-a1b2c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"wsp-a1b2c3d4e5f6\"\n    result = client.workspaces.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"wsp-a1b2c3d4e5f6\";\nconst result = await client.workspaces.get(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Get a workspace (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetWorkspace","tags":["Workspaces"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The workspace id."}],"responses":{"200":{"description":"The workspace.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"patch":{"summary":"Rename a workspace","description":"Renames a workspace. Its id does not change, so the API keys confined to it and the computers in it are untouched. Owners only, with an account-wide API key: a key confined to a workspace is refused (`403`), its own workspace included.\n\nThe name follows the rules a create does, and a name the account already uses is a `400`, as is any field but `name`. An id this credential cannot see is a `404`, the same as one that does not exist.\n\nRequires the `owner` role or higher.","operationId":"renameWorkspace","tags":["Workspaces"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The workspace id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"The new name: 1 to 40 characters after trimming, unique within the account.","minLength":1}},"required":["name"]}}}},"responses":{"200":{"description":"The workspace, renamed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"wsp-a1b2c3d4e5f6","name":"customer-acme-prod","created_at":"2026-09-16T12:00:00.000Z"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 4 KiB (4096 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X PATCH \"${base_url%/}\"'/workspaces/wsp-a1b2c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"customer-acme-prod\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"wsp-a1b2c3d4e5f6\"\n    body = json.loads(\"{\\\"name\\\":\\\"customer-acme-prod\\\"}\")\n    result = client.workspaces.rename(id, body[\"name\"])\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"wsp-a1b2c3d4e5f6\";\nconst body = {\"name\":\"customer-acme-prod\"};\nconst result = await client.workspaces.rename(id, body.name);\nconsole.log(JSON.stringify(result, null, 2));"}]},"delete":{"summary":"Delete a workspace","description":"Deletes an empty workspace, and in the same step REVOKES every API key confined to it, whoever holds them: a key has nothing left to reach once its workspace is gone, and it is never widened to the account instead. The answer says how many were revoked. Owners only, with an account-wide API key: a key confined to a workspace is refused (`403`), its own workspace included.\n\nWebhook subscriptions confined to the workspace (those created with a key scoped to it) are deleted with it in the same step, with every delivery record they hold, pending ones included, as `DELETE /webhooks/{id}` does; the answer does not count them. Account-wide subscriptions are not affected.\n\nA workspace that still holds computers cannot be deleted: the answer is `409` with `reason: \"unsupported\"`, saying how many, and nothing is deleted or revoked. The count includes a computer whose unfinished workspace change is moving it into this workspace. Move those computers with `PUT /computers/{id}/workspace` or in the dashboard, or delete them, first. When the platform cannot confirm the workspace is empty because part of it did not answer, the answer is `503`, also with nothing deleted; try again shortly.\n\nA workspace holding active secrets also answers `409` with `reason: \"unsupported\"`, saying how many. Delete those secrets explicitly with `DELETE /secrets/{id}` first, using their `workspace_id` and current `revision_id`, then retry. The refusal preserves the workspace, its keys and all secret data. Deleted secrets do not block workspace deletion. Deleting a secret frees an active slot but does not reduce the lifetime count of 1000 created secrets.\n\nThis safeguard prevents new orphan secrets; it does not repair secrets orphaned by older workspace deletions. Those remain unreachable: listing or creating with a deleted `workspace_id` is a `400`, and reading, replacing or deleting one of its secrets is a `404`. They still count toward the 100 active secrets per account.\n\nSnapshots taken in it are kept. An account-wide key or the dashboard can still clone them: a clone built from the disk (a disk snapshot, or `memory: false`) lands in no workspace, and one that would resume a memory snapshot’s session is refused with `409` (see `POST /snapshots/{id}/clone`).\n\nRequires the `owner` role or higher.","operationId":"deleteWorkspace","tags":["Workspaces"],"x-rate-cost":5,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The workspace id."}],"responses":{"200":{"description":"Deleted, with the count of keys it revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceDeleted"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"revoked_keys":2}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to any API key confined to a workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"409":{"description":"Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running.\n\nWHETHER RETRYING HELPS IS IN THE BODY, NOT THE STATUS, and this is the one thing to take from this entry. Some of these describe a state that is passing: a guest agent still inside its boot window, a guest agent busy with another call, a move already running on the account. Those clear on their own and the same request works a moment later. The rest describe a DECISION about the request you sent — the size does not fit, the computer is the wrong one for this, the saved session cannot travel — and no amount of retrying turns one of those into a yes. A client that treats 409 as uniformly transient loops forever on half of them.\n\nWhere the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline.\n\nEvery 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names.\n\nThe ones worth knowing by name, because each has a next step that is not \"wait\":\n\nA resize past what the computer’s host can run carries `move`, an object rather than a message: `{\"required\":true,\"possible\":true}` means another host in this region could run it and the computer has to be moved there first — `POST /computers/{id}/move` with the same sizing fields is how you agree to that, and it is a separate call because relocating a computer is not something a resize should do to you quietly. `possible:false` means nowhere in the region can, and the size is the thing to change.\n\nThe move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: \"contention\"` — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `reason: \"unavailable\"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: \"unsupported\"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.\n\nThe workspace still holds computers or active secrets, with `reason: \"unsupported\"` and a count. Computers include any that an unfinished workspace change is moving into it. Nothing was deleted: no key was revoked and no webhook subscription removed. Move or delete its computers and explicitly delete its secrets before retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"holds-computers":{"summary":"Move or delete its computers first; nothing was changed.","value":{"error":"This workspace still holds 2 computers. Move or delete them first: a workspace that holds computers cannot be deleted.","reason":"unsupported","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"holds-secrets":{"summary":"Delete the active secrets first; the workspace, keys and secret data are preserved.","value":{"error":"This workspace still holds 1 secret. Delete it first: a workspace that holds secrets cannot be deleted.","reason":"unsupported","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}},"503":{"description":"Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nWhether the workspace holds computers could not be confirmed. Nothing was deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/workspaces/wsp-a1b2c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"wsp-a1b2c3d4e5f6\"\n    result = client.workspaces.delete(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"wsp-a1b2c3d4e5f6\";\nconst result = await client.workspaces.delete(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWorkspacesById","tags":["Workspaces"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The workspace id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/workspaces/{id}/members":{"get":{"summary":"List a workspace’s members","description":"The account’s accepted members, oldest member first. Workspaces do not divide people: members share every workspace at their account role, subject to seat and sign-in suspension. Outstanding invitations not yet accepted are left out. A member waiting for a seat, because the plan has fewer seats than the account has people, is listed with `seat_suspended: true` and cannot act on the account until their seat is restored. `suspended` reports global sign-in suspension independently; either or both flags can be `true`.\n\nAn API key confined to a workspace cannot list members (`403`), because the list is the whole account’s. Use an account-wide key.\n\nRequires the `viewer` role or higher.","operationId":"listWorkspaceMembers","tags":["Workspaces"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The workspace id."}],"responses":{"200":{"description":"The members, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMember"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"user_id":"usr-0123456789abcdef","email":"dana@example.com","name":"Dana Whitfield","role":"owner","accepted_at":"2026-08-01T09:00:00.000Z","suspended":false,"seat_suspended":false},{"user_id":"usr-89abcdef01234567","email":"sam@example.com","name":null,"role":"viewer","accepted_at":"2026-09-02T15:30:00.000Z","suspended":false,"seat_suspended":true}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer to any API key confined to a workspace, with no `reason`: the members are the whole account’s.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/workspaces/wsp-a1b2c3d4e5f6/members' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"wsp-a1b2c3d4e5f6\"\n    result = client.workspaces.members(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"wsp-a1b2c3d4e5f6\";\nconst result = await client.workspaces.members(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List a workspace’s members (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListWorkspaceMembers","tags":["Workspaces"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The workspace id."}],"responses":{"200":{"description":"The members, oldest first.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer to any API key confined to a workspace, with no `reason`: the members are the whole account’s.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWorkspacesByIdMembers","tags":["Workspaces"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The workspace id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/whoami":{"get":{"summary":"Who am I","description":"The credential this request was made with: the person holding it, the account and role it acts with, the workspace it is confined to (`null` for an account-wide key), and the key itself.\n\nThe role is the holder’s role now, not when the key was minted. A suspended account may still call this, and `account.status` says so.\n\nA workspace-scoped key is not told its holder’s email address or name, nor the account’s name, because such a key may be in an end customer’s hands: each is `null`. Its `account.plan` is `null` too; `GET /account` reports the plan and its ceilings.\n\nRequires the `viewer` role or higher.","operationId":"getWhoami","tags":["API keys"],"x-rate-cost":1,"responses":{"200":{"description":"The credential and its holder.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Whoami"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"user":{"id":"usr-0123456789abcdef","email":"dana@example.com","name":"Dana Whitfield"},"account":{"id":"acc-d5e6f7a8b9c0d1e2","name":"Acme","plan":"studio","status":"active"},"role":"owner","workspace":null,"key":{"id":"key-a1b2c3d4e5f6","name":"ci","prefix":"com_1a2b3c4d…","created_at":"2026-09-20T08:00:00.000Z","last_used_at":"2026-09-25T14:05:00.000Z","workspace_id":null,"workspace_name":null,"manage_keys":true,"minted_by_key_id":null}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/whoami' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.account.whoami()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.account.whoami();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"Who am I (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headGetWhoami","tags":["API keys"],"responses":{"200":{"description":"The credential and its holder.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nA suspended account may still make this read: a `403` here is about the credential or the person holding it, never the account’s suspension.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsWhoami","tags":["API keys"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/api-keys":{"get":{"summary":"List API keys","description":"Your API keys on this account, newest first. Needs a key with `manage_keys` on: that is set for a key from the dashboard (Settings → Credentials → API keys → the key’s menu → Allow managing keys), is off for every key until then, and every other key is refused this route with `403`.\n\nOnly the holder’s own keys, and only those acting on this account. A key confined to a workspace lists the keys confined to that same workspace and no others.\n\nRequires the `viewer` role or higher.","operationId":"listApiKeys","tags":["API keys"],"x-rate-cost":1,"responses":{"200":{"description":"The keys, newest first. The raw keys are never listed.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}},"examples":{"success":{"summary":"Illustrative successful response.","value":[{"id":"key-a1b2c3d4e5f6","name":"ci","prefix":"com_1a2b3c4d…","created_at":"2026-09-20T08:00:00.000Z","last_used_at":"2026-09-25T14:05:00.000Z","workspace_id":null,"workspace_name":null,"manage_keys":true,"minted_by_key_id":null}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to a key that does not have `manage_keys` on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body \"${base_url%/}\"'/api-keys' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    result = client.api_keys.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst result = await client.apiKeys.list();\nconsole.log(JSON.stringify(result, null, 2));"}]},"head":{"summary":"List API keys (headers only)","description":"The `GET` of this path with no response body. It is authorized exactly as the `GET` is and answers its statuses and headers, never a body: an error’s correlation is in `X-Request-ID` alone. `Range` is ignored. It runs the `GET`’s own read and discards the body, so it touches whatever that read touches, the guest included: it is not a lighter way to poll.\n\nRequires the `viewer` role or higher.","operationId":"headListApiKeys","tags":["API keys"],"responses":{"200":{"description":"The keys, newest first. The raw keys are never listed.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to a key that does not have `manage_keys` on.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}}},"post":{"summary":"Create an API key","description":"Mints a key for the key’s holder on this account. The answer carries the raw key in `raw`, once: it is stored only as a hash and no route returns it again. Needs a key with `manage_keys` on, and a member or owner role (a viewer mints from the dashboard).\n\nA key cannot mint a key with `manage_keys` on (`403`): only the dashboard grants it. Nor can it mint one wider than itself. Without `workspace_id` a key confined to a workspace mints into that workspace, and naming any other scope, `null` included, is a `403`. An account-wide key may name any workspace of the account (an unknown one is a `400`) or none, for another account-wide key.\n\nThe body is optional. No body, or one of nothing but whitespace, mints an unnamed key exactly as `{}` does.\n\nThe account holds at most 1000 keys, whoever minted them; the next is a `400`.\n\nRequires the `member` role or higher.","operationId":"createApiKey","tags":["API keys"],"x-rate-cost":1,"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":["string","null"],"description":"A label, up to 60 characters; longer is cut; control, bidirectional and invisible formatting characters (such as U+200B and U+2060) are refused (`400`), while the joiners U+200C and U+200D and the variation selectors (such as U+FE0F) that emoji and some scripts need are allowed. Optional; `null` is the same as leaving it out."},"workspace_id":{"type":["string","null"],"description":"The workspace to confine the new key to, or leave it out. See above for a workspace-scoped key."},"manage_keys":{"type":"boolean","description":"Must be `false` or absent. `true` is refused: only the dashboard grants it."}}}}}},"responses":{"201":{"description":"The key, with the raw key in `raw`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"key-0f1e2d3c4b5a","name":"ci-rotation","prefix":"com_01234567…","created_at":"2026-09-25T14:06:00.000Z","last_used_at":null,"workspace_id":null,"workspace_name":null,"manage_keys":false,"minted_by_key_id":"key-a1b2c3d4e5f6","raw":"com_0123456789abcdef0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"400":{"description":"The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso, with no `reason`: this key does not have `manage_keys` on; the body asked for `manage_keys: true`; a workspace-scoped key named another scope; or the key’s holder has not verified their email address (`Verify your email before creating an API key.`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.\n\nThis route reads at most 4 KiB (4096 bytes) of request body, less than the general cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X POST \"${base_url%/}\"'/api-keys' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-raw '{\"name\":\"ci\"}'"},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport json, os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    body = json.loads(\"{\\\"name\\\":\\\"ci\\\"}\")\n    result = client.api_keys.create(name=body[\"name\"])\n    print(\"Key returned once; store result.key securely without logging it.\")"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst body = {\"name\":\"ci\"};\nconst result = await client.apiKeys.create({name: body.name});\nconsole.log(\"Key returned once; store result.key securely without logging it.\");"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsApiKeys","tags":["API keys"],"security":[],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}},"/api-keys/{id}":{"delete":{"summary":"Revoke an API key","description":"Revokes one of your keys on this account; anything using it is refused from its next request. Needs a key with `manage_keys` on, and a member or owner role. A key outside your reach — another person’s, another account’s, or for a workspace-scoped key any key not confined to its workspace — is the same `404` as one that does not exist.\n\nA key may revoke itself; the request that does it is its last.\n\nThe SSH keys added through this key go with it: the gateway refuses them to any connection it has not already let through, and each is taken off the computers it was written into as each computer receives the new key list. Until a computer does, a connection already let through can still reach it; the answer does not wait for the computers. No other API key is revoked: the keys it minted keep working, and each names it in `minted_by_key_id`; the dashboard can revoke them with it, or in one step after it has been revoked.\n\nRequires the `member` role or higher.","operationId":"revokeApiKey","tags":["API keys"],"x-rate-cost":1,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The API key id."}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ack"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing.","value":{"error":"Authentication required","reason":"missing","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"invalid":{"summary":"Platform bearer credential invalid.","value":{"error":"invalid API key","reason":"invalid","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}},"revoked":{"summary":"Platform bearer credential revoked.","value":{"error":"API key revoked","reason":"revoked","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: \"revoked\"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.\n\nAlso the answer, with no `reason`, to a key that does not have `manage_keys` on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on a `403` from the re-check that the credential’s authority still holds (a lost membership, a suspended person), which is answered before the budget is checked.","schema":{"type":"integer"}}}},"404":{"description":"No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}}}},"405":{"description":"This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"method-not-allowed":{"summary":"Unsupported method on this public path; HEAD returns these headers without a body.","value":{"error":"method not allowed","request_id":"3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Allow":{"description":"Only this public path’s methods, in stable order; GET implies HEAD and known paths include OPTIONS. Resource existence is not checked.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: \"contention\"` (see `POST /snapshots/{id}/clone`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying. Usually the rate budget; a route with its own cap (the daily build allowance, the build-event stream cap, store capacity, pending secret-inheriting forks) sends its own wait on this status, and the message names which limit applied. Where a route’s failures say what the wait is worth there, that is the answer.","schema":{"type":"integer"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}},"RateLimit-Limit":{"description":"The budget binding on this request: the per-key half, or the account-wide figure. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left of that budget after this request. Absent on an unexpected `500`.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until that budget has refilled. Absent on an unexpected `500`.","schema":{"type":"integer"}}}}},"x-codeSamples":[{"lang":"Curl","label":"curl","source":"base_url=\"${MANDALA_BASE_URL:-}\"\nif [ -z \"$base_url\" ]; then base_url='https://app.mandala.computer/api/v1'; fi\ncurl --fail-with-body -X DELETE \"${base_url%/}\"'/api-keys/key-a1b2c3d4e5f6' \\\n  -H \"Authorization: Bearer $MANDALA_API_KEY\""},{"lang":"Python","label":"Python — Mandala SDK 0.17.0","source":"# Python 3.10+: pip install mandala-computer==0.17.0\nimport os\nfrom mandala_computer import Client\n\nwith Client(api_key=os.environ[\"MANDALA_API_KEY\"], base_url=os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\") as client:\n    id = \"key-a1b2c3d4e5f6\"\n    result = client.api_keys.revoke(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.17.0","source":"// Node.js 22+: npm install mandala-computer@0.17.0\nimport { Client } from \"mandala-computer\";\n\nconst apiKey = process.env.MANDALA_API_KEY;\nif (!apiKey) throw new Error(\"Set MANDALA_API_KEY\");\nconst client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"key-a1b2c3d4e5f6\";\nconst result = await client.apiKeys.revoke(id);\nconsole.log(JSON.stringify(result, null, 2));"}]},"options":{"summary":"List the allowed methods","description":"Public path metadata. No credential is needed or read, nothing is looked up — whatever the path names need not exist — and the answer is not metered. `Allow` lists this path’s public methods, with `HEAD` wherever there is a `GET`, and `OPTIONS`.","operationId":"optionsApiKeysById","tags":["API keys"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The API key id."}],"responses":{"204":{"description":"The path’s methods, in `Allow`. No body.","headers":{"Allow":{"description":"This public path’s methods in stable order, e.g. `GET, HEAD, POST, OPTIONS`.","schema":{"type":"string"}},"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"404":{"description":"Not a public path: a path segment the API refuses for its characters, a separator inside a path parameter for one, is answered as no such path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}},"500":{"description":"The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID (a UUID); caller IDs are ignored.","schema":{"type":"string","format":"uuid"}}}}}}}},"webhooks":{"event":{"post":{"summary":"A delivery to your endpoint","description":"Sent to each subscription’s `url` (see `POST /webhooks`), signed with the Standard Webhooks headers `webhook-id`, `webhook-timestamp` and `webhook-signature`. Answer with a 2xx within 10 seconds; anything else is retried.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Accepted. Any other answer, or none in 10 seconds, is retried."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key from Settings, e.g. `com_...`."}},"schemas":{"BrowserConnection":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-f0-9]{32}$"},"url":{"type":"string","description":"The CDP WebSocket URL. Supply token in the Authorization: Bearer header."},"token":{"type":"string","description":"A delegated browser capability. Keep it out of URLs, logs and model messages."},"expires_at":{"type":"string","format":"date-time","description":"Legacy: attachment and socket expiry. Version 2: immutable alias of attach_expires_at; the active lease is separate."},"lifecycle_version":{"type":"integer","const":2},"server_time":{"type":"string","format":"date-time"},"attach_expires_at":{"type":"string","format":"date-time"},"lease_expires_at":{"type":"string","format":"date-time"},"absolute_expires_at":{"type":"string","format":"date-time"},"lease_seconds":{"type":"integer","minimum":60,"maximum":7200},"idle_timeout_seconds":{"type":"integer","const":0,"description":"No independent inactivity timeout; lease and absolute deadlines still apply."}},"required":["id","url","token","expires_at"],"additionalProperties":false},"BrowserSessionLease":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-f0-9]{32}$"},"lifecycle_version":{"type":"integer","const":2},"server_time":{"type":"string","format":"date-time"},"attach_expires_at":{"type":"string","format":"date-time"},"lease_expires_at":{"type":"string","format":"date-time"},"absolute_expires_at":{"type":"string","format":"date-time"},"lease_seconds":{"type":"integer","minimum":60,"maximum":7200},"idle_timeout_seconds":{"type":"integer","const":0}},"required":["id","lifecycle_version","server_time","attach_expires_at","lease_expires_at","absolute_expires_at","lease_seconds","idle_timeout_seconds"],"additionalProperties":false},"AccountQuota":{"type":"object","properties":{"scope":{"type":"string","const":"account","description":"Whole-account aggregates, including for workspace-scoped viewers."},"advisory":{"type":"boolean","const":true,"description":"An observation, never an admission guarantee or reservation. Admission — of a create or clone, a start, restart, restore or resume, a resize, a move, a never-suspend pin or a template build — also counts two things left out here: a create or clone that reached a host whose answer was lost, or was a `5xx` that cannot rule out a computer was built (see `POST /computers`), and a reservation of running vCPU and RAM for each seed one of your own templates boots for fast launches. The seed has 30 seconds to start, but its reservation stays charged until the platform confirms both that the seed is gone and that its permission to start has run out. While its host cannot be asked — unreachable, halted, removed or registered again — it stays charged. Either can make a `402` name more than these numbers show."},"observed_at":{"type":"string","format":"date-time","description":"UTC collection completion time; not a consistency token."},"plan":{"type":"object","properties":{"id":{"type":"string","description":"Effective plan catalogue identifier."},"label":{"type":"string","description":"Effective plan label."}},"required":["id","label"],"additionalProperties":false},"limits":{"type":"object","properties":{"max_computers":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Maximum kept computers; existing starts also check active or reserved count."},"vcpu_pool":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Account vCPU pool ceiling."},"ram_pool_mb":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Running or reserved guest RAM ceiling, MB."},"disk_pool_gb":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Configured disk pool ceiling, GB. Shared by computers’ configured disks and the template builds the account keeps."},"snapshot_storage_bytes":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Snapshot storage ceiling, exact bytes (plan GB multiplied by 1024 cubed)."}},"required":["max_computers","vcpu_pool","ram_pool_mb","disk_pool_gb","snapshot_storage_bytes"],"additionalProperties":false},"per_computer":{"type":"object","properties":{"max_vcpu":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Per-computer vCPU maximum."},"max_ram_mb":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Per-computer RAM maximum, MB."},"max_disk_gb":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Per-computer disk maximum, GB."}},"required":["max_vcpu","max_ram_mb","max_disk_gb"],"additionalProperties":false},"capabilities":{"type":"object","properties":{"windows":{"type":"boolean","description":"Whether the plan permits Windows computers."}},"required":["windows"],"additionalProperties":false},"complete":{"type":"object","properties":{"computers":{"type":"boolean","description":"Every relevant computer inventory answered with valid numbers."},"snapshots":{"type":"boolean","description":"Every relevant snapshot inventory answered with valid indexed byte sizes."}},"required":["computers","snapshots"],"additionalProperties":false},"usage":{"type":"object","properties":{"kept_computers":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"All kept computers, including stopped, suspended, building and deletion states still in inventory. Null when computers are incomplete."},"configured_vcpu":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Sum of configured vCPU over all kept computers, regardless of power state. Null when computers are incomplete."},"configured_disk_gb":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Sum of configured disk GB; not filesystem occupancy. Null when computers are incomplete."},"running_or_reserved_computers":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Computers with positive running or reserved RAM, including pending starts. Null when computers are incomplete."},"running_or_reserved_vcpu":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Configured vCPU of computers with positive running or reserved RAM; starts check this separately from configured vCPU. Null when computers are incomplete."},"running_or_reserved_ram_mb":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Guest RAM in use or reserved for a pending start, MB. Explicit zero overrides power state; legacy unknown state conservatively counts configured RAM. Null when computers are incomplete."},"snapshot_storage_bytes":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Indexed stored bytes, including pending/deleting snapshots and each physical copy during handover. Capture placeholders contribute explicit zero indexed bytes; private in-flight capture reservations are not exposed. Null when snapshots are incomplete."}},"required":["kept_computers","configured_vcpu","configured_disk_gb","running_or_reserved_computers","running_or_reserved_vcpu","running_or_reserved_ram_mb","snapshot_storage_bytes"],"additionalProperties":false},"remaining":{"type":"object","properties":{"kept_computers":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Maximum minus kept count, clamped at zero. Null when computers are incomplete."},"configured_vcpu":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"vCPU ceiling minus configured vCPU, clamped at zero. Not a start allowance after downgrade. Null when computers are incomplete."},"configured_disk_gb":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Disk ceiling minus configured disk, GB, clamped at zero. Kept template builds share the pool and are not subtracted here, so a create or resize can still be refused for storage. Null when computers are incomplete."},"running_or_reserved_ram_mb":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"RAM ceiling minus running or reserved RAM, MB, clamped at zero. Null when computers are incomplete."},"snapshot_storage_bytes":{"type":["integer","null"],"minimum":0,"maximum":9007199254740991,"description":"Headroom against indexed stored bytes, clamped at zero; not an available capture reservation. Null when snapshots are incomplete."}},"required":["kept_computers","configured_vcpu","configured_disk_gb","running_or_reserved_ram_mb","snapshot_storage_bytes"],"additionalProperties":false}},"required":["scope","advisory","observed_at","plan","limits","per_computer","capabilities","complete","usage","remaining"],"additionalProperties":false},"PlatformSignal":{"oneOf":[{"type":"object","properties":{"seq":{"type":"integer","minimum":0,"maximum":9007199254740991},"cursor":{"type":"string","description":"Opaque checkpoint after this event. Never join executions by PID.","maxLength":2048},"at":{"type":"string","format":"date-time"},"computer":{"type":"string"},"source":{"type":"string","const":"daemon"},"type":{"type":"string","const":"process.exited"},"data":{"oneOf":[{"type":"object","properties":{"pid":{"type":"integer","minimum":1,"maximum":2147483647},"execution_id":{"type":"string","description":"Optional stable execution identity; never inferred from PID.","pattern":"^exec_[a-f0-9]{32}$"},"exit_code":{"type":"integer","minimum":-2147483648,"maximum":2147483647}},"required":["pid","exit_code"],"additionalProperties":false},{"type":"object","properties":{"pid":{"type":"integer","minimum":1,"maximum":2147483647},"execution_id":{"type":"string","description":"Optional stable execution identity; never inferred from PID.","pattern":"^exec_[a-f0-9]{32}$"},"lost":{"type":"boolean","const":true}},"required":["pid","lost"],"additionalProperties":false}]}},"required":["seq","cursor","at","computer","source","type","data"],"additionalProperties":false},{"type":"object","properties":{"seq":{"type":"integer","minimum":0,"maximum":9007199254740991},"cursor":{"type":"string","description":"Opaque checkpoint after this event. Never join executions by PID.","maxLength":2048},"at":{"type":"string","format":"date-time"},"computer":{"type":"string"},"source":{"type":"string","const":"daemon"},"type":{"type":"string","const":"computer.started"},"data":{"type":"object","properties":{"status":{"type":"string","const":"running"},"previous":{"type":"string","enum":["running","stopped","suspended","building","build-failed","half-removed"],"description":"Absent for a first observation."}},"required":["status"],"additionalProperties":false}},"required":["seq","cursor","at","computer","source","type","data"],"additionalProperties":false},{"type":"object","properties":{"seq":{"type":"integer","minimum":0,"maximum":9007199254740991},"cursor":{"type":"string","description":"Opaque checkpoint after this event. Never join executions by PID.","maxLength":2048},"at":{"type":"string","format":"date-time"},"computer":{"type":"string"},"source":{"type":"string","const":"daemon"},"type":{"type":"string","const":"computer.stopped"},"data":{"type":"object","properties":{"status":{"type":"string","const":"stopped"},"previous":{"type":"string","enum":["running","stopped","suspended","building","build-failed","half-removed"],"description":"Absent for a first observation."}},"required":["status"],"additionalProperties":false}},"required":["seq","cursor","at","computer","source","type","data"],"additionalProperties":false},{"type":"object","properties":{"seq":{"type":"integer","minimum":0,"maximum":9007199254740991},"cursor":{"type":"string","description":"Opaque checkpoint after this event. Never join executions by PID.","maxLength":2048},"at":{"type":"string","format":"date-time"},"computer":{"type":"string"},"source":{"type":"string","const":"daemon"},"type":{"type":"string","const":"computer.suspended"},"data":{"type":"object","properties":{"status":{"type":"string","const":"suspended"},"previous":{"type":"string","enum":["running","stopped","suspended","building","build-failed","half-removed"],"description":"Absent for a first observation."}},"required":["status"],"additionalProperties":false}},"required":["seq","cursor","at","computer","source","type","data"],"additionalProperties":false},{"type":"object","properties":{"seq":{"type":"integer","minimum":0,"maximum":9007199254740991},"cursor":{"type":"string","description":"Opaque checkpoint after this event. Never join executions by PID.","maxLength":2048},"at":{"type":"string","format":"date-time"},"computer":{"type":"string"},"source":{"type":"string","const":"daemon"},"type":{"type":"string","const":"computer.idle"},"data":{"type":"object","properties":{"idle_seconds":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"A daemon idle-policy judgment, not proof that no work is running."}},"required":["idle_seconds"],"additionalProperties":false}},"required":["seq","cursor","at","computer","source","type","data"],"additionalProperties":false}]},"PlatformSignalGap":{"type":"object","properties":{"cursor":{"type":"string"},"at":{"type":"string","format":"date-time"},"type":{"type":"string","const":"gap"},"computer":{"type":"string"},"source":{"type":"string","const":"daemon"},"data":{"type":"object","properties":{"oldest_cursor":{"type":"string"},"detail":{"type":"string","const":"events happened that this computer can no longer replay; anything you were waiting on may already have occurred"}},"required":["detail"]}},"required":["cursor","at","type","computer","source","data"]},"PlatformSignalPage":{"type":"object","properties":{"computer":{"type":"string"},"from":{"type":"string","description":"Opaque start checkpoint, or current head on baseline/reset."},"cursor":{"type":"string","description":"Next opaque checkpoint, including filtered-out ring rows."},"events":{"type":"array","items":{"$ref":"#/components/schemas/PlatformSignal"},"maxItems":100},"more":{"type":"boolean","description":"Another eligible event is owed; poll with cursor."},"baseline":{"type":"boolean","description":"The first read starts at current head with no historical replay."},"gap":{"$ref":"#/components/schemas/PlatformSignalGap"},"supported":{"type":"array","items":{"type":"string","enum":["process.exited","computer.started","computer.stopped","computer.suspended","computer.idle"]}},"retention":{"type":"string","const":"ephemeral"}},"required":["computer","from","cursor","events","more","baseline","supported","retention"]},"Error":{"type":"object","properties":{"error":{"type":"string","description":"What went wrong, in a sentence meant to be shown to a person."},"request_id":{"type":"string","description":"Server-generated correlation ID, a UUID, also sent in X-Request-ID. HEAD carries it only in the header.","format":"uuid"},"code":{"type":"string","description":"Optional route-specific detail. Execution reads use execution_unavailable for unknown, expired or replaced IDs on a computer the caller can see (a computer outside the caller’s scope answers 404 `computer not found` with no code) and output_unavailable for a guest log that cannot be opened. Input uses not_an_input_action for a computer tool’s `screenshot` or `zoom` posted as an action. Other routes may use other codes."},"operation_id":{"type":"string","description":"On a keyed lifecycle call’s `idempotency_in_progress` / `idempotency_outcome_unknown` refusal, or on a `5xx` answer to it whose outcome is unknown: the operation reserved for it; read `GET /operations/{id}`. Absent otherwise — a `5xx` whose key was released, because the call was refused before it was sent anywhere, was a create or clone the host confirms it refused before building anything, or was a start, restore or suspend the host confirms it refused before changing anything, names none — when the operation could not be recorded, and on two unknown-outcome `5xx` answers that do not name it: an unexpected `500` from the platform itself while the call was being sent, and a `5xx` from upstream that was not JSON. After either, resend under the same key: it answers `409 idempotency_outcome_unknown` with the `operation_id`."},"reason":{"type":"string","description":"At API-key ingress, missing means no Authorization attempt, invalid means an unusable credential, and revoked means a matching retained revocation hash (at most 90 days and 100,000 newest hashes globally). Fresh revoked classification does not recover identity. Known suspension or lost membership remains 403.\n\nWhat KIND of refusal this is, in one word, where the platform can say. One of `contention` (something is in flight and the same request works once it finishes), `starting` (the computer is running and its guest agent has not answered yet inside the bounded boot window — two minutes from a start, a restart, or a reboot from inside the guest), `draining` (on a `503` from a call that starts an agent run: the platform is about to restart for an update and is not starting new runs — send the same request again after its `Retry-After`), `unavailable` (the computer is not running — **start it**; this never clears by waiting), `unsupported` (waiting will not change the answer: this computer cannot do it at all, or it needs another step first, which the message names, such as a stop or removing its secret bindings; on `POST /templates`, a template ceiling), `running` (the computer is running and this needs it stopped, as a resize or a move does — **stop it**; the mirror of `unavailable`, and a start would be refused again), `exists` (a create-only file upload whose path is already taken — send it again with `overwrite=true` only if you mean to replace that file; on `POST /templates`, a ref already spoken for), `name_taken` (a secret of that name is already in that scope — choose another name, or replace the one there), `stale_revision` (the secret changed since you read it — read it again for its current `revision_id`; on `PUT /computers/{id}/secrets`, the bound list changed since the `version` you sent was read), `stale_capture` (the screenshot capture you named is no longer held — take a new screenshot and name its capture) or `revoked` (the authority this request arrived with no longer holds).\n\nThe first three are worth retrying and the rest are not. `revoked` is the one that is about YOU rather than about the computer: the authority the request arrived with stopped holding while it was in flight. **Do not send the same call again** — and note that a long request can be refused this way PART WAY THROUGH, after work has already been done, so read the body rather than assuming nothing happened.\n\nTHE STATUS BESIDE IT SAYS WHETHER A REPLACEMENT CREDENTIAL IS THE MISSING PIECE, which is the one thing you can act on without asking anybody.\n\n`401` — the credential this request carried is not usable. Getting another one is NECESSARY: the key was deleted or rotated, the session expired or was signed out elsewhere, or the workspace a scoped key was issued against was deleted, which deletes that key with it. It is not promised to be SUFFICIENT — your authority on the account may have ended in the same moment, and a `401` is not evidence that nothing about the account changed, since that deleted-workspace case is somebody on the account having acted.\n\n`403` — a replacement credential is not the missing piece. The membership was removed, the role was lowered, the account was suspended, or the PERSON was suspended. The first three are the account’s to answer and an owner can; the fourth is not, and no new credential and nobody on the account can undo it. A suspended person meets this AT THE DOOR as well as part way through, and deliberately: the sign-in door refuses them too, so `401` there would be a promise nothing can keep.\n\nA refusal over your ROLE carries `revoked` only when the role CHANGED while the request was in flight. A role that was never sufficient is still a `403`, with the same sentence naming what you have and what the route needs, and NO word — because `revoked` says the authority you arrived with is gone, and on an ordinary permission denial that would be false.\n\nNEITHER STATUS PREDICTS WHAT YOUR NEXT REQUEST WILL ANSWER. Each describes the refusal in your hand. Sign in again after a `401` and you may meet a `403`, or another `401` — if the membership you lost was your only one, the new credential resolves to no account and is refused as unauthenticated all over again. Read the refusal you have; do not infer the next one.\n\nThis pair was not reliable before and this entry said so — being removed from an account used to answer `401`, and a credential invalidated mid-call used to answer `403`, each the wrong half. Both are fixed. If you wrote the defensive branch that entry asked for, treating both as \"do not send this again\", it is still correct — the word alone still says that.\n\n**Absent means the platform has no word for this refusal** — treat that, and any value you do not recognise, as no answer and fall back to your own rule. Not every route classifies its refusals yet."}},"required":["error","request_id"]},"OpenAIError":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/OpenAIErrorDetail"},"request_id":{"type":"string","description":"The server-generated X-Request-ID (a UUID) for this request.","format":"uuid"}},"required":["error","request_id"]},"OpenAIErrorDetail":{"type":"object","properties":{"message":{"type":"string","description":"What went wrong, in a sentence."},"code":{"type":"integer","description":"The HTTP status, repeated inside the body as OpenAI clients expect."},"reason":{"type":"string","description":"Present only when the platform can classify the failure, and the same word as `reason` on every other refusal here — see `Error` for the whole set. A run refused part way through because the API key it was running on stopped carrying the account carries `revoked`, and a run that never started because the computer could not be reached carries whatever word describes THAT — `unavailable` or `contention` — so do not read this field as meaning `revoked` alone. An addition to OpenAI’s shape, so a client that does not know it ignores it."},"usage":{"type":"object","description":"Tokens spent before the run failed, in the OpenAI shape."},"steps":{"type":"array","items":{"type":"object"},"description":"What the loop had done when it failed."},"agent":{"type":"object","description":"The same detail in our own shape, including `computer_id`."}}},"AgentResult":{"type":"object","properties":{"cost_usd":{"type":"number","description":"Cumulative AI credit charge in USD, present on funded runs only."},"steps":{"type":"integer","description":"How many actions the loop took — a count; the actions themselves are `steps_taken`."},"stop":{"type":"string","description":"Why the loop ended: `end_turn` (the model finished), `max_steps` (it reached `max_steps`), `refusal` (the model declined), `pause_turn` (the model kept pausing its turn), `rate_limited` (your rate budget ran out mid-run), `credits_exhausted` (funded credits unavailable), `max_cost` (the next reservation would exceed max_cost_usd), or another stop reason the model itself gave, such as `max_tokens`. OpenAI also reports `incomplete` for an unfinished response and `safety_check` when action approval is needed. Only `end_turn` means the task was finished; the others are a run that stopped, and the work it did to the desktop stands."},"text":{"type":"string","description":"The model’s closing text — its answer, or why it could not get there."},"usage":{"$ref":"#/components/schemas/AgentUsage"},"thread_id":{"type":"string","description":"The thread this exchange was kept in; send it as `thread_id` to continue. Absent with `store: false`."},"steps_taken":{"type":"array","items":{"$ref":"#/components/schemas/AgentStep"},"description":"Every action, in order. On the `stream: false` body only; the stream’s `done` event carries the other fields, and each action arrived before it as a `step` event."}},"required":["steps","stop","text","usage"]},"AgentStep":{"type":"object","properties":{"cost_usd":{"type":"number","description":"Cumulative funded run charge in USD. Multiple tools in one reply repeat the same total; do not sum these fields."},"credits_remaining_usd":{"type":"number","description":"Available account AI credits after this model call; may be negative after an overrun. Omitted if the post-debit balance lookup is unavailable."},"n":{"type":"integer","description":"Its position in the run, from 1."},"tool":{"type":"string","description":"The tool the model reached for, e.g. `computer` or `bash`."},"action":{"type":"string","description":"The provider-native action within it, e.g. `left_click`, `zoom` or OpenAI `click`. Absent for `bash`."},"detail":{"type":"string","description":"What the platform did with it, in one line."},"error":{"type":"string","description":"Present when an action failed or was refused. On an Anthropic run the model is told and the loop goes on. On an OpenAI run a failed batch or `bash` command ends the run as a failure, after the actions the desktop acknowledged; input is not replayed."}},"required":["n","tool"]},"AgentUsage":{"type":"object","properties":{"input_tokens":{"type":"integer","description":"Input tokens, the cache reads and writes below included."},"output_tokens":{"type":"integer"},"cache_read_tokens":{"type":"integer"},"cache_write_tokens":{"type":"integer","description":"Provider-reported cache writes; zero only when absent. OpenAI reads input_tokens_details.cache_write_tokens."}},"required":["input_tokens","output_tokens","cache_read_tokens","cache_write_tokens"]},"AgentCreditRefusal":{"type":"object","properties":{"error":{"type":"string","description":"`Insufficient AI credits`."},"top_up_url":{"type":"string","description":"Where to add credits: a path, not a full URL — `/billing`, the dashboard’s billing page on this same origin."},"request_id":{"type":"string","description":"The server-generated X-Request-ID (a UUID) for this request.","format":"uuid"}},"required":["error","top_up_url","request_id"]},"AgentFailure":{"type":"object","properties":{"cost_usd":{"type":"number","description":"Cumulative AI credit charge before the funded run failed."},"top_up_url":{"type":"string","description":"Where to add credits, when the run failed for want of them: a path, not a full URL — `/billing`, the dashboard’s billing page on this same origin."},"error":{"type":"string","description":"What went wrong, in a sentence."},"request_id":{"type":"string","description":"The server-generated X-Request-ID (a UUID) for this request.","format":"uuid"},"reason":{"type":"string","description":"The one-word classification, where there is one — see `Error`."},"usage":{"$ref":"#/components/schemas/AgentUsage"},"thread_id":{"type":"string","description":"The thread the failed run was kept in, when it acted on the desktop first or continued a thread you named."},"steps_taken":{"type":"array","items":{"$ref":"#/components/schemas/AgentStep"},"description":"The actions already taken when it failed. They are real, and so is what they did."}},"required":["error","request_id","usage","steps_taken"]},"ChatCompletion":{"type":"object","properties":{"id":{"type":"string","description":"This run’s completion id, the same one every chunk of a streamed run carries."},"object":{"type":"string","const":"chat.completion"},"created":{"type":"integer","description":"Unix seconds."},"model":{"type":"string","description":"Always `computer-use-agent`: a fixed label for this loop, NOT the model that ran it — that is the `model` you sent, or our default."},"choices":{"type":"array","items":{"$ref":"#/components/schemas/ChatCompletionChoice"},"description":"Exactly one."},"usage":{"type":"object","properties":{"prompt_tokens":{"type":"integer","description":"The run’s input tokens, cache reads and writes included."},"completion_tokens":{"type":"integer"},"total_tokens":{"type":"integer","description":"The two above added together."}},"required":["prompt_tokens","completion_tokens","total_tokens"]},"agent":{"type":"object","properties":{"computer_id":{"type":"string"},"steps":{"type":"integer","description":"How many actions the loop took."},"stop":{"type":"string","description":"Why the loop ended, in our own words — the `stop` of `AgentResult`. **Read this, not `finish_reason`, to know whether the task finished**: only `end_turn` means it did."},"thread_id":{"type":"string","description":"The thread this exchange was kept in, as on `AgentResult`. Absent with `store: false`."}},"required":["computer_id","steps","stop"]}},"required":["id","object","created","model","choices","usage","agent"]},"ChatCompletionChoice":{"type":"object","properties":{"index":{"type":"integer","description":"Always `0`."},"message":{"type":"object","properties":{"role":{"type":"string","const":"assistant"},"content":{"type":"string","description":"The model’s closing text — its answer, or why it could not get there."}},"required":["role","content"]},"finish_reason":{"type":"string","enum":["stop","length","content_filter"],"description":"Our `agent.stop` in OpenAI’s vocabulary: `max_steps`, `max_tokens`, `model_context_window_exceeded`, `pause_turn` and `incomplete` are `length`, `refusal` and `safety_check` are `content_filter`, and everything else is `stop` — **`rate_limited` included**, so a run that ran out of rate budget reports `stop` like a finished one. Read `agent.stop` to tell them apart."}},"required":["index","message","finish_reason"]},"Activity":{"type":"object","properties":{"activity_id":{"type":"string","description":"Immutable activity ID; a request identity, never an execution idempotency key."},"account_id":{"type":"string"},"computer_id":{"type":"string"},"workspace_id":{"type":["string","null"],"description":"The actual workspace when the request was admitted."},"channel":{"type":"string","enum":["api","session-api","managed-agent"],"description":"Who sent the request. `api`: an API key, or a connected app acting through one. `session-api`: the dashboard, signed in. `managed-agent`: a hosted agent run — its summary row (action `agent`), and a row for each input or `bash` command it sent to the desktop."},"route":{"type":"string","enum":["input","exec","window","clipboard","agent"]},"action":{"type":"string","enum":["agent","input","move","click","button-down","button-up","drag","scroll","type","paste","key","hold-key","exec","background-exec","window","focus","raise","minimize","maximize","unmaximize","close","window-move","resize","clipboard"]},"state":{"type":"string","enum":["pending","acknowledged","exited","accepted","refused","unknown"]},"received_at":{"type":"string","description":"Control-plane receipt time, UTC."},"observed_at":{"type":"string","description":"Control-plane observation time, UTC."},"revision":{"type":"integer","minimum":1,"maximum":9007199254740991,"description":"Monotonic row revision, including late result links. Does not change action observation time."},"has_results":{"type":"boolean","description":"Small link hint; expand the passive results detail for current availability."},"funded":{"type":"boolean","description":"Present on managed-agent run summaries: whether the run used account AI credits."},"cost_usd":{"type":"number","description":"Total funded AI charge on a managed-agent run summary. Zero for BYOK, whose model bill is external."},"dispatched_at":{"type":"string","description":"First physical eligible transport attempt, when observed. Does not prove guest execution."},"elapsed_ms":{"type":"integer","description":"Monotonic request elapsed time, including network and admission; not guest execution duration."},"reason":{"type":"string","enum":["body-too-large","invalid-body","local-refusal","transport","unclassified","invalid-result","result-too-large","wait-ended","pending-expired"]},"http_status":{"type":"integer","description":"Known response status. A dispatched error does not prove the action had no effect."},"exit_code":{"type":"integer","description":"Present only for a known synchronous command exit, including signed failures. Timeout sentinel values are omitted.","minimum":-2147483648,"maximum":2147483647},"execution_id":{"type":"string","description":"Optional immutable execution ID returned by the original request; never inferred from PID. No output is fetched to populate history."}},"required":["activity_id","account_id","computer_id","workspace_id","channel","route","action","state","received_at","observed_at"]},"ActivityResults":{"type":"object","properties":{"activity_id":{"type":"string","pattern":"^act_[a-f0-9]{32}$"},"revision":{"type":"integer","minimum":1,"maximum":9007199254740991},"more":{"type":"boolean"},"items":{"type":"array","maxItems":8,"items":{"oneOf":[{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^res_[a-f0-9]{32}$"},"kind":{"type":"string","const":"background-output"},"association":{"type":"string","const":"background_execution_output"},"availability":{"type":"string","const":"unavailable"}},"required":["id","kind","association","availability"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string","pattern":"^res_[a-f0-9]{32}$"},"kind":{"type":"string","const":"background-output"},"association":{"type":"string","const":"background_execution_output"},"availability":{"type":"string","const":"available"},"captured_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"stdout":{"type":"object","properties":{"bytes":{"type":"integer","minimum":0,"maximum":4194304},"retained_truncated":{"type":"boolean"},"upstream_truncated":{"type":"null"}},"required":["bytes","retained_truncated","upstream_truncated"],"additionalProperties":false},"stderr":{"type":"object","properties":{"bytes":{"type":"integer","minimum":0,"maximum":4194304},"retained_truncated":{"type":"boolean"},"upstream_truncated":{"type":"null"}},"required":["bytes","retained_truncated","upstream_truncated"],"additionalProperties":false},"diagnostic":{"type":"object","properties":{"bytes":{"type":"integer","minimum":0,"maximum":65536},"truncated":{"type":"boolean"}},"required":["bytes","truncated"],"additionalProperties":false},"observation":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","const":"running"}},"required":["status"],"additionalProperties":false},{"type":"object","properties":{"status":{"type":"string","const":"exited"},"exit_code":{"type":"integer","minimum":-2147483648,"maximum":2147483647}},"required":["status","exit_code"],"additionalProperties":false}]}},"required":["id","kind","association","availability","captured_at","expires_at","stdout","stderr","diagnostic","observation"],"additionalProperties":false}]},{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^res_[a-f0-9]{32}$"},"kind":{"type":"string","const":"synchronous-output"},"association":{"type":"string","const":"synchronous_exec_response"},"availability":{"type":"string","const":"unavailable"}},"required":["id","kind","association","availability"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string","pattern":"^res_[a-f0-9]{32}$"},"kind":{"type":"string","const":"synchronous-output"},"association":{"type":"string","const":"synchronous_exec_response"},"availability":{"type":"string","const":"available"},"captured_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"stdout":{"type":"object","properties":{"bytes":{"type":"integer","minimum":0,"maximum":4194304},"retained_truncated":{"type":"boolean"},"upstream_truncated":{"type":"boolean"}},"required":["bytes","retained_truncated","upstream_truncated"],"additionalProperties":false},"stderr":{"type":"object","properties":{"bytes":{"type":"integer","minimum":0,"maximum":4194304},"retained_truncated":{"type":"boolean"},"upstream_truncated":{"type":"boolean"}},"required":["bytes","retained_truncated","upstream_truncated"],"additionalProperties":false},"diagnostic":{"type":"null"},"observation":{"type":"object","properties":{"status":{"type":"string","const":"exited"},"exit_code":{"type":"integer","minimum":-2147483648,"maximum":2147483647}},"required":["status","exit_code"],"additionalProperties":false}},"required":["id","kind","association","availability","captured_at","expires_at","stdout","stderr","diagnostic","observation"],"additionalProperties":false}]},{"oneOf":[{"type":"object","properties":{"id":{"type":"string","pattern":"^art_[a-f0-9]{32}$"},"kind":{"type":"string","const":"artifact"},"association":{"type":"string","const":"caller_selected_artifact"},"availability":{"type":"string","const":"unavailable"}},"required":["id","kind","association","availability"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string","pattern":"^art_[a-f0-9]{32}$"},"kind":{"type":"string","const":"artifact"},"association":{"type":"string","const":"caller_selected_artifact"},"availability":{"type":"string","const":"available"},"captured_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"bytes":{"type":"integer","minimum":0,"maximum":67108864}},"required":["id","kind","association","availability","captured_at","expires_at","bytes"],"additionalProperties":false}]}]}}},"required":["activity_id","revision","more","items"],"additionalProperties":false},"ActivityHealth":{"type":"object","properties":{"recording_started_at":{"type":"string","description":"When this store enabled recording; earlier requests are not reconstructed."},"earliest_retained_at":{"type":["string","null"],"description":"Earliest retained row in the current read scope."},"count_truncated":{"type":"boolean","description":"Account history has crossed a row-count retention boundary."},"age_truncated":{"type":"boolean","description":"Account history has crossed an age retention boundary."},"capture":{"type":"string","enum":["available","degraded"]},"completeness":{"type":"string","enum":["best-effort"],"description":"Operational history, without an audit-grade completeness guarantee, including across process loss."},"gap_at":{"type":["string","null"],"description":"Most recent account-level capture gap within retention. It identifies no unverified computer."},"recovered_at":{"type":["string","null"],"description":"Last observed recovery from a capture gap."}},"required":["recording_started_at","earliest_retained_at","count_truncated","age_truncated","capture","completeness","gap_at","recovered_at"]},"ActivityPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}},"next_cursor":{"type":["string","null"],"description":"Continuation for older rows, or further changes in changes mode."},"changes_cursor":{"type":"string","description":"Revision cursor including completion updates to older rows."},"gap":{"type":"boolean","description":"The change cursor expired or its window was truncated. Discard it and refresh the visible history page."},"health":{"$ref":"#/components/schemas/ActivityHealth"}},"required":["items","next_cursor","changes_cursor","gap","health"]},"BrowserProxy":{"type":"object","properties":{"server":{"type":"string","description":"The proxy, as `http://host:port`, `https://host:port` or `socks5://host:port`, lower-cased as stored. The host is a name or an IPv4 address, the port is explicit, and there is nothing else in it: no path, and never a username or password — a URL carrying one is refused, because this setting is returned by every read of the computer; give an `http://` proxy its password with `credentials_secret_id` instead. `https://` is TLS to the proxy itself, for a proxy that listens for it. HTTPS sites are tunnelled end to end through an `http://` proxy too; `https://` also encrypts, between the browser and the proxy, which sites are visited and any plain-HTTP page. With `https://`, Chromium is given the proxy as it is, and Firefox — whose settings cannot name a TLS proxy — a proxy auto-config (PAC) script at `/etc/mandala/browser-proxy.pac` that its locked settings point at, answering `HTTPS host:port` and applying `bypass` exactly as Chromium does (with no DNS lookup: an address range matches only a URL whose host is an address), and it does not fall back to connecting directly when the proxy is down. Private, link-local (including the cloud metadata address), CGNAT, multicast and unspecified addresses are refused, since a computer cannot reach them; `127.0.0.1` and `localhost` are allowed and mean the computer itself, so a forwarder of your own running on it can be the proxy."},"credentials_secret_id":{"type":"string","description":"For an upstream that asks for a username and password: the id of a secret in your store whose value is `user:password` (a trailing newline is ignored). Only with an `http://` proxy for now, and not one on the computer’s own loopback. The computer holds the id, never the value, and the value reaches it one way only — the secret must be bound to the computer as a FILE, `{\"secret_id\": …, \"file\": \"<name>\"}` in its `secrets` (at create, or with `PUT /computers/{id}/secrets`), or the setting is refused with 400. So everything a bound secret does, this does: it is delivered as the computer starts, a computer’s first secrets can be bound only while it is stopped, a binding added to a running computer arrives at its next restart, and replacing the secret’s value reaches a running computer within seconds where its image takes live file updates (otherwise at its next start). While the setting names it, a change to the computer’s secrets that drops that file binding is refused with 409. The browsers are pointed at a forwarder on the computer’s loopback, `127.0.0.1:29380`, which reads `/run/mandala-secrets/user/files/<name>` on every connection and sends the upstream `Proxy-Authorization: Basic`; `bypass` applies as without credentials."},"bypass":{"type":"array","items":{"type":"string"},"description":"Hosts the browser reaches directly, at most 64: `example.com` (that host), `*.example.com` or `.example.com` (its subdomains), an IPv4 address, a range such as `192.0.2.0/24` or `fd00::/8`, or `<local>` (names with no dot). Stored in a canonical spelling, which a read returns: trimmed and lower-cased, `.example.com` as `*.example.com`, a range masked to its network (`192.0.2.7/24` is `192.0.2.0/24`), and a bare IPv6 address as its `/128`; entries that are then the same are kept once. A range matches a URL whose host is an address in it, never a name that resolves into it. `localhost` and loopback addresses always bypass the proxy. So do link-local addresses (`169.254.0.0/16`, `fe80::/10`) in Chromium, and in Firefox with an `https://` proxy."}},"required":["server"]},"EgressProxy":{"type":"object","properties":{"server":{"type":"string","description":"The proxy, as `http://host:port` (one that takes `CONNECT`), `https://host:port` (the same, spoken to over TLS) or `socks5://host:port`, lower-cased as stored. The host is a fully qualified name or a public IPv4 address, and the port is explicit; there is nothing else in it — no path, and never a username or password, which is refused because this setting is returned by every read of the computer (name a secret in `credentials_secret_id` instead). The computer’s HOST connects to it, so private, loopback, link-local (including the cloud metadata address), CGNAT, multicast, reserved and unspecified addresses are refused, and a name is checked again on every connection after it resolves: one that resolves to such an address is not dialled, and the computer’s connection fails. An `https://` proxy’s certificate is verified against the host’s public certificate authorities for the proxy’s name (or, for an address, the address); one that does not verify fails every connection, and nothing is sent directly."},"credentials_secret_id":{"type":"string","description":"For an upstream that asks for a username and password: the id of a secret in your store — in the computer’s workspace, or account-wide — whose value is `user:password` (at most 4096 bytes, no control characters; one trailing newline is ignored). A secret that is not there, or does not hold that, is refused with 400. Three more refusals, each changing nothing: 409 where delivering secrets is not switched on for the deployment, 409 when the secret’s value cannot be read on this control plane (replace its value), and 503 when secrets are not enabled at all. The computer never receives the value, and the secret is NOT bound to it: the computer’s HOST holds the value in memory and signs in to the proxy for it, with `Proxy-Authorization: Basic` on the `CONNECT` (`http://`, `https://`) or SOCKS5 username/password authentication (each part 1 to 255 bytes), and nothing inside the computer can read it. With `http://` or `socks5://` the credentials cross the network to the proxy in clear text; prefer `https://` when naming credentials. Replacing the secret’s value reaches the host within seconds, for connections opened after it; connections already open stay open. Deleting the secret removes the value from the host, and the computer’s connections are then refused until the setting names a secret that exists. While a running computer’s host does not hold the value — just after a create or a change, or after the host restarts — `egress_proxy_pending` is `true` and every connection the computer opens is closed; none is ever sent directly or without the credentials. Once a computer’s workspace is deleted, its egress credentials resolve account-wide only, so a proxy naming one of that workspace’s own secrets is held closed (`egress_proxy_pending` `true`) until it is saved again naming an account-wide secret."}},"required":["server"]},"Computer":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier, e.g. `vm-a1b2c3d4e5f6`."},"name":{"type":"string","description":"The name you gave it."},"status":{"type":"string","description":"`running`, `stopped`, `suspended`, `building`, `build-failed` or `half-removed`. The last is a computer whose deletion stopped partway and took its disk with it: every call that needs a disk is refused on it, it will never start again, and deleting it again is what clears it. A client that treats an unknown status as stopped will offer a start this API always refuses, so read these as a closed set and everything outside it as not startable. A `running` computer can be paused by its host without its status changing. When the host is low on disk space, the guests writing fastest are paused, and when it is critically low every running computer on it is paused, including ones that wrote nothing. Screenshots, input and commands stall or fail meanwhile, and the computer resumes by itself once space recovers. A suspend or memory snapshot that fails can also leave it paused: always when the failure says “this computer remains paused because the host is low on disk space”, and other failures can, at any free space. While the platform holds it for disk-space recovery, start, restart, suspend and snapshot answer `503` (“waiting for disk-space recovery”) until it resumes by itself once that recovery can finish, and a stop discards the paused session. Nor does any computer start or resume on a host that is low on disk space: a start, a restart of a stopped computer and the resume that using a suspended computer sets off answer `503` (“this host is low on disk space…”), and a restore answers `503` too, until space on that host has been freed."},"os":{"type":"string","description":"`linux` or `windows`."},"desktop":{"type":"string","description":"The display protocol this computer’s desktop speaks: `wayland`, `x11`, or absent (X11, or a host too old to say); branch on `=== \"wayland\"`. Screenshots, input and `exec` behave identically either way. What differs on a Wayland computer is that a window `id` is the compositor’s address rather than an X window id — opaque and handed back the same way, but not comparable across the two — and that moving or resizing a window the compositor is TILING is refused rather than silently ignored, because a tiled window’s geometry belongs to the layout. Read it here rather than from the template: a computer keeps the image it was built from, and a template’s version can advance underneath it."},"template":{"type":"string","description":"The template this computer was built from."},"cpu":{"type":"integer","description":"Cores."},"ram_mb":{"type":"integer"},"disk_gb":{"type":"integer"},"running_ram_mb":{"type":"integer","description":"The guest memory this computer is holding against your plan’s running pool: `ram_mb` while a process is live, or while a start or resume has been admitted and has not launched yet, and 0 otherwise. Non-zero BEFORE `status` says `running`, which is the point of publishing it — admission reserves the memory, and `status` is read from the guest process, which does not exist yet. So `stopped` with a non-zero value here is a cold boot on its way up, and `suspended` with one is a resume on its way up; 0 beside either means nothing has been admitted. Not proof that nothing will be: a start still waiting on this computer’s lifecycle lock has reserved nothing yet. ABSENT rather than 0 wherever this API cannot say — a host that could not be reached, a response written before the computer was read back (a create that answers with a start error, `PATCH`, either clone), or a host too old to report it. Absence means unknown, never zero."},"resolution":{"type":"string","description":"The screen this guest renders at, as `WIDTHxHEIGHTxDEPTH`. This is the coordinate space every click and every screenshot is in — size your model prompts from it rather than by decoding a PNG. Chosen at create and fixed for the life of the computer."},"workspace_id":{"type":"string","description":"The workspace this computer is in. Absent when it is in none. It can change: a stopped computer can be moved to another workspace, or to none, with `PUT /computers/{id}/workspace` or from the dashboard."},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"build":{"$ref":"#/components/schemas/Build","description":"Present only while `status` is `building` or `build-failed`."},"suspended":{"$ref":"#/components/schemas/Suspended","description":"Present only while `status` is `suspended`."},"idle_suspend_min":{"type":"integer","description":"How long this computer may go untouched before its host suspends it. Absent on a computer with no override, which follows whatever its host is sweeping at. The host default is deliberately not reported in its place — it is a property of the host and it changes when an operator changes it."},"snapshot_schedule":{"$ref":"#/components/schemas/Schedule"},"browser_proxy":{"$ref":"#/components/schemas/BrowserProxy","description":"The proxy this computer’s browsers are sent through — Chromium, Chrome where it is installed, and Firefox — as a managed policy each reads when it starts, locked so the browser’s own settings cannot override it. Only the browsers: `exec`, a terminal and every other program reach the network directly. Absent when none is set. Set at create or with `PATCH /computers/{id}`; a clone does not inherit it."},"egress_proxy":{"$ref":"#/components/schemas/EgressProxy","description":"The proxy ALL of this computer’s outbound TCP is sent through, taken on its host at the computer’s network port, so nothing inside the computer is configured and nothing there can opt out: `exec`, terminals, package managers and browsers alike. Each connection is tunnelled to the host and port it was going to, by name where the connection says one (the TLS server name, or an HTTP `Host`) so the proxy resolves it, and otherwise by address. It FAILS CLOSED: when the proxy is down or refuses, the connection fails and nothing is sent directly. UDP to the internet is dropped (so QUIC falls back to TCP, and NTP and other UDP do not work), as is ICMP. DNS lookups still go to the platform’s resolver and are not proxied. Connections through the proxy are closed when the setting changes, and when the computer stops. One owner’s computers on a host hold at most 512 proxied connections at once together (256 through an `https://` proxy), and fewer when the host is busy with other owners’; past that a new connection is closed at once. A connection whose server speaks first (SMTP, MySQL, an FTP control channel) waits about 250 ms for the computer to send first, then is tunnelled by address. A connection one side has finished is closed after 60 seconds of silence from the other side. Absent when none is set. Set at create or with `PATCH /computers/{id}`; a clone does not inherit it."},"egress_proxy_pending":{"type":"boolean","description":"Present and `true` while a running computer’s `egress_proxy` names `credentials_secret_id` and its host does not hold that secret’s value yet — just after a create or a change, after the host restarts, or once the secret is deleted. Every connection the computer opens meanwhile is closed, never sent directly; it usually clears within seconds. It does NOT clear by itself once the computer’s workspace is deleted: its egress credentials then resolve account-wide only, so a proxy naming one of that workspace’s own secrets is held closed until it is saved again naming an account-wide secret. Never present on a computer that is not running."},"browser_proxy_pending":{"type":"boolean","description":"Present and `true` while a running computer’s guest does not have its `browser_proxy` yet — just after a create, a start or a change — or still has the files of one just removed. Wait for it to go away before starting a browser that must use the proxy. A browser already running when the setting changes applies it at its next start (Chromium usually within seconds). Never present on a computer that is not running: the setting is applied as it starts."},"state":{"type":"string","description":"Whether this computer exists, as the platform’s own record has it — a different question from `status`, which is what its host says it is doing. `live`: its host lists it. `unreachable`: its host has not answered — on a listing row, for this request; the computer is most likely fine and nothing has been done to it. `deleting`: a delete was sent and not yet answered. `deleted` and `lost` are terminal, and are only ever shown when asked for with `state=`: `deleted` is a delete that was answered, `lost` is a computer whose host was written off by an operator while it was unreachable. Present on listing rows; absent on a single computer, which is served by its host and is therefore live."},"deleted_at":{"type":"string","description":"RFC 3339 timestamp of the answered delete. Present once `state` has been `deleted`."},"lost_at":{"type":"string","description":"RFC 3339 timestamp of the write-off. Present once `state` has been `lost`."},"unreachable":{"type":"boolean","description":"Present and `true` only on a row the platform served from its own record because the host holding this computer could not be reached. Such a row carries the computer’s identity — `name`, `os`, `template`, size, `workspace_id`, `created_at`, `state` — and nothing only its host knows: no `status`, no `resolution`. Its `state` is `unreachable`, or `deleting` when a delete is in flight for it. A listing with any such row carries `X-GC-Incomplete`, so you only ever see this after asking for a partial answer with `allow_partial=1`."},"secrets":{"type":"array","items":{"type":"object","properties":{"secret_id":{"type":"string","description":"The secret, as `GET /secrets` lists it."},"revision_id":{"type":"string","description":"The revision last delivered to this computer, or the latest when it was bound if none has been delivered since. Every start and restart delivers each secret's latest value and moves this to it."},"env":{"type":"string","description":"The environment variable the value lands under in the desktop session, for a secret bound as one."},"file":{"type":"string","description":"For a secret bound as a file instead: its name under `/run/mandala-secrets/user/files`."}},"required":["secret_id","revision_id"]},"description":"The secrets bound to this computer — at create, or since with `PUT /computers/{id}/secrets` — as references: which secret, which revision of it, and the environment variable or file each lands under. Never a value. Present only on a computer that holds secrets; every field in this group is absent on one that does not, except `secrets_pending` while it is `true`. On any host that seals saved sessions a computer that holds secrets restarts — by this API or from inside it — and gets its secrets again as it comes back up; a restart of a stopped one is refused (`409`, `reason: \"unavailable\"`): start it instead, which delivers its secrets. It suspends — by hand or when idle — and can be memory-snapshotted. A disk snapshot restored onto it starts it with its secrets delivered afresh; a memory snapshot restored onto it resumes holding what it held when the snapshot was taken, and is refused if it was taken without sealing or the platform will no longer open it (see `POST /snapshots/{id}/restore`). Taking disk snapshots, cloning, moving between hosts and deletion work as usual, and a clone starts with no secrets bound, unless it is a fork of one of its memory snapshots made with `inherit_secrets: true`."},"secrets_generation":{"type":"integer","description":"How many delivering starts this computer has had. Each cold start of a computer that holds secrets is one generation; the receipt below names the generation it is for, so a receipt whose generation is behind this one is from an earlier start."},"secrets_applied":{"type":"object","properties":{"generation":{"type":"integer","description":"The delivering start this receipt is for."},"applied_at":{"type":"string","description":"RFC 3339 timestamp: when the values reached the desktop session."},"revisions":{"type":"object","additionalProperties":{"type":"string"},"description":"Secret id to the revision id that was delivered, for every binding."}},"required":["generation","applied_at","revisions"],"description":"The receipt that the bound values reached this computer’s desktop session: which start, when, and which revision of each secret. This is the confirmation — a start that was taken but whose delivery did not land has no receipt for its generation and `secrets_error` says why. Absent before the first delivery."},"secrets_error":{"type":"string","description":"Why the last delivering start was stopped, in one fixed sentence naming the class of failure and never a value. A start whose delivery fails is stopped by the host and stays stopped until you start it again; this is the reason. Cleared when the next delivering start begins. Absent when the last delivery landed or none has been tried."},"secrets_pending":{"type":["boolean","null"],"description":"Whether this computer’s desktop is missing a change to its secrets that a restart would deliver: a binding, an env name or file, or a newer value it does not hold — or, once every secret has been removed, whether its running or saved session still holds the values it had. Compared against the receipt, so it clears once any delivery lands — a start, a restart from anywhere, a reboot inside it, or a secret file replaced while it runs — and stays after one that failed. A value replaced for a secret bound as a file is replaced live on a running computer, and is pending only if that did not land. One bound as an environment variable is the same on an image that hands new shells the current values: once they see it, it is not pending, though programs already running — the desktop session among them — keep the value they started with until a restart; on any other image it waits for a restart. A change that removed a secret, or moved one to another revision, stays pending until a restart either way, since the running session may still hold the old value. Only ever true of a running or suspended computer: a suspended one resumes with the values it had, and a stopped one gets everything current when it starts, so it is never pending. `null` when it could not be checked — the current values could not be read, or the computer’s last delivery predates the record of the names it was made under and it has been rebound since; a restart makes it known. A delivery still on its way is not pending — wait on `secrets_delivering` for that. Present whenever `secrets` is, and on a computer with no secrets bound only while true."},"secrets_delivering":{"type":"boolean","description":"Whether values are on their way into this computer’s desktop right now. A computer comes back `running` a few seconds before its secrets land, and a command run in between sees them unset; after a create, a start or a restart, wait for this to be `false` before relying on them. Also true from the moment a restart, or a reboot inside the computer, clears them until they are delivered again, and while a replaced value is written to a file. Reported by the computer’s host, which is doing the delivering. False on a computer that is not running and after a delivery that failed (`secrets_error` says why, and the computer is stopped). After a restart or a reboot it can also go back to `false` with nothing delivered and no `secrets_error`: that is a computer that has not come back up three minutes after it, and its secrets are not in it — restart it, or stop and start it. While it reads `true` only because a restarted computer has not come back up yet, a further restart is accepted, since that is the way out of one that does not; once the delivery itself has begun, a restart answers 409 until it lands. Present whenever `secrets` is."}}},"Build":{"type":"object","properties":{"started":{"type":"string","description":"RFC 3339 timestamp."},"source":{"type":"string","description":"What the disk is being copied from."},"failed":{"type":"string","description":"Why the build failed, when it did."}}},"PublishedTemplate":{"type":"object","properties":{"ref":{"type":"string","description":"`namespace/name@version`. What you pass as `template` to create a computer."},"doc_digest":{"type":"string","description":"Identifies the document, as `sha256:…`. Two publishes of the same digest are the same template, which is what makes republishing an unchanged document a no-op rather than a conflict."},"document":{"type":"object","description":"The document itself, in its canonical form — the bytes `doc_digest` is over. Key order and whitespace may differ from what you sent; nothing else does."},"template":{"$ref":"#/components/schemas/Template","description":"The catalogue row this document describes — the same shape `GET /templates` lists."},"versions":{"type":"array","items":{"type":"string"},"description":"Every version of this template, newest first. Read one with `?version=`."},"published_at":{"type":"string","description":"RFC 3339 timestamp. Absent on a template we publish."}}},"RetiredTemplates":{"type":"object","properties":{"retired":{"type":"array","items":{"type":"string"},"description":"The refs that were retired, newest version first. Never empty — an empty retire is a `404`."},"retired_at":{"type":"string","description":"RFC 3339 timestamp. One value: everything in `retired` went in the same write."},"versions":{"type":"array","items":{"type":"string"},"description":"The versions of this name still published, newest first. Empty means the name is gone — nothing resolves `namespace/name` any more."},"templates":{"type":"integer","description":"How many templates the account holds now. This is the number the per-account ceiling is against."},"refs_claimed":{"type":"integer","description":"How many refs this account has ever claimed, live and retired together. It does NOT go down when you retire — a retired ref still counts, because it can never be published again — and there is a much larger ceiling on it than on `templates`. Reported here because this is the only place the two numbers can be seen moving differently."}}},"TemplateBuild":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier, e.g. `bld-a1b2c3d4e5f6`."},"ref":{"type":"string","description":"The document this was built from, as `namespace/name@version`."},"status":{"type":"string","description":"`running`, `succeeded` or `failed`."},"error":{"type":"string","description":"Why it failed, when it did. For a failing `run:` step this is the end of that step’s own output."},"started_at":{"type":"string","description":"RFC 3339 timestamp."},"finished_at":{"type":"string","description":"RFC 3339 timestamp. Absent while it is still running."}}},"BuildProgress":{"type":"object","properties":{"id":{"type":"string","description":"The build this describes."},"status":{"type":"string","description":"`running`, `succeeded` or `failed` — the job’s own status, restated so one poll answers both questions."},"done":{"type":"boolean","description":"Whether to stop polling. Derived from `status` and not from `phase`: a phase is read out of the build’s log, which your own `run:` steps write into, and only the job decides whether a build worked."},"phase":{"type":"string","description":"Where the build is in itself: `planning`, `staging`, `copying`, `building`, `publishing`, and then `published`, `reused` or `failed`. The long ones are `copying` — the base image is several gigabytes — and `building`, which is the one with steps in it.\n\n`unknown` is the one remaining value, and it means the build finished without keeping a step-by-step record — every build from before this endpoint existed is one. It is not reported as `published` because a build that reused an existing image succeeds too, and that distinction lived in the record that is missing. `status` is still the answer."},"step":{"type":"integer","description":"Which step is running, 1-based, or the one that failed. `0` before the build reaches the first."},"of":{"type":"integer","description":"How many steps there are."},"steps":{"type":"array","items":{"$ref":"#/components/schemas/BuildStep"},"description":"Every step, in order, whatever its status — so the whole list renders from the first read."},"note":{"type":"string","description":"One line about the phase, or why a failed build failed."},"error":{"type":"string","description":"Why it failed, when it did. The same value `GET builds/{id}` gives."},"updated_at":{"type":"string","description":"RFC 3339. When the build last MOVED, and not when this was last read — a build whose steps have stopped advancing is a build whose `updated_at` stops advancing."},"unmatched":{"type":"boolean","description":"Present and true only when this fleet could not recognise its own build tool’s output, so the per-step position is unavailable. The build itself is unaffected and `status` is still the answer."}}},"BuildStep":{"type":"object","properties":{"n":{"type":"integer","description":"Its position, 1-based."},"kind":{"type":"string","description":"`apt`, `run`, `file`, `mkdir`, `env` for the environment block, or `finish` for the cleanup every build ends with."},"label":{"type":"string","description":"What the step does, from your own document — the packages, the path, or the first real line of the script."},"status":{"type":"string","description":"`pending`, `running`, `done`, `failed`, or `skipped` for one an earlier failure meant we never reached.\n\n`unknown` is the rare one: this build’s step record was lost mid-build — a host that lost power between the write and the flush — and rebuilt from your document, so these steps ran and what became of them is not recoverable. It is not reported as `done`, which would claim they succeeded, or as `pending`, which would claim they never ran."},"started_at":{"type":"string","description":"RFC 3339. Absent until it starts."},"finished_at":{"type":"string","description":"RFC 3339. Absent until it ends."}}},"Suspended":{"type":"object","properties":{"at":{"type":"string","description":"When the session was saved, RFC 3339. A resumed guest’s clock is set to the current time within a few seconds of the resume, through its guest agent; the desktop panel’s clock can show the saved time for up to a minute after that."}}},"Vnc":{"type":"object","properties":{"url":{"type":"string","description":"A websocket URL that opens this desktop with full control — keyboard and pointer. Credential included, so treat it as one. Absent for a viewer.\n\nThe CLIPBOARD bridge is provisioned on some computers and not others, and `clipboard` below reports that initial configuration. It is not a live guest-health check; read its caveats if the guest has been modified after creation.\n\n`GET` and `PUT /computers/{id}/clipboard` are the route to build on if you only want to write it once, because they need nothing of the HARDWARE — no cold boot, and no permission from a browser. They are not universal either, and what they want is different: a Linux guest with a display and the clipboard tool in the image — `xclip` on X11, `wl-clipboard` (`wl-paste`/`wl-copy`) on Wayland — since they drive the guest’s own desktop session. A computer built from a golden that predates that tool is refused with a 400 that says so, and Windows is refused outright. That is a much smaller set than the socket’s two conditions, and unlike them it is stated in the answer rather than left for you to infer. Where the socket does carry the clipboard the two do not fight over it — the endpoints write the same `CLIPBOARD` selection the agent then offers onward.\n\nThose endpoints replace what was documented here as a recipe over `POST /computers/{id}/exec` in the `desktop` session. Do not go back to it. Public `exec` runs a LOGIN shell, which sources the guest user's own profile onto the same stdout your command prints to, ahead of it — fine for running a command, and fatal for reading a value, since an `echo` left in a `.profile` corrupts the answer and a deliberate one forges it. The clipboard endpoints do not share that stream. The write was worse: an X selection belongs to a live process, so the holder had to outlive the exec and have its output redirected or the call hung to its full `timeout_s`; the text had to be base64 and quoted or an apostrophe in it ended the shell word; and the result had to be polled for, because being granted a selection is asynchronous and reading back too soon returns the PREVIOUS clipboard. The `PUT` does all of that, confirms the selection was taken before it answers, and bills once."},"view_url":{"type":"string","description":"The same socket, watch only. The daemon drops input on it, so a browser cannot type.\n\nThe guest’s CLIPBOARD does not reach a watcher either, and that is enforced rather than asked for: the daemon takes the clipboard capability out of the connection as it is negotiated, so a patched client gains nothing by asking. Worth knowing if you embed this — whatever the person using the desktop copies, including a password, is not visible to anyone holding this URL."},"token":{"type":"string","description":"The credential inside `url`, for a client that builds its own noVNC URL. Absent for a viewer."},"view_token":{"type":"string","description":"The credential inside `view_url`, and what a viewer is given in place of `token`. Read-only against a patched client rather than only against ours, because the filtering is the daemon’s — and that covers the clipboard coming back as well as the input going out. See `view_url`."},"embed_url":{"type":"string","description":"Our hosted viewer, watch only — drop it in an `<iframe>` or the `<com-desktop>` element to put a live desktop in your own product without building a VNC client.\n\nPrefer the element if you are choosing, and prefer it especially if one frame of yours shows different computers at different times. Both open the same page, but the element REPLACES its frame whenever the computer or the credential changes, where an `<iframe>` you re-point keeps one document and one session history. Each URL you assign to that frame becomes a history entry of it, and this URL carries a credential. The viewer takes it out of its own URL as soon as it has read it, and a browser is entitled to refuse that. Browsers cap how many history rewrites they will make, and the two caps are not the same shape. Safari allows a hundred every ten seconds, raises an error past that, and counts them across the whole page — so your own `pushState` calls draw on the allowance the viewer needs. Chrome allows two hundred per frame every ten seconds and refuses without telling your code at all. Either way an entry the browser refused on keeps what it was holding. Your user pressing Back can then land on that entry, naming a desktop you had already replaced or detached, and it reconnects with the permissions that credential was issued with. The element discards the frame instead, so there is no entry IN THAT FRAME to go back to.\n\nThat is the whole of what the element gives you, and the rest is yours, so it is worth saying plainly rather than leaving you to find out. It covers the frame it owns; it does not reach your own history. If your page pushed its own entries while a frame was showing one computer, going Back in YOUR app can restore a frame at that URL, and markup or server-rendered state that carries a token in an attribute comes back with it. Keep the credential out of your routes and out of anything you render server-side, and set it from code on the element. A reload is entitled to work, which is the same reason a credential in a link somebody emails is live until the machine restarts."},"terminal_url":{"type":"string","description":"A websocket that opens an interactive terminal — a PTY in the guest, running a login shell as the desktop user. Binary frames are the terminal’s bytes in both directions; send a text frame `{\"type\":\"resize\",\"cols\":120,\"rows\":32}` to resize, and you’ll receive `{\"type\":\"exit\",\"code\":0}` when the shell ends. Size it on the way in with `&cols=`/`&rows=` (80x24 by default) rather than resizing immediately after connecting. Sessions persist across disconnects: add `&session=<name>` to reattach to a named session (default `main`), and its recent output is replayed. Carries the same controlling credential as `url`, so treat it as one. Absent for a viewer, and on Windows guests.\n\nEach binary input frame may contain up to 1 MiB. The guest holds at most 1 MiB of pending input per session while a busy or stopped program is not reading. If the next frame will not fit, that entire frame and subsequent input from that attachment are rejected: the client receives `{\"type\":\"error\",\"reason\":\"input_overflow\",\"message\":\"...\"}` and its attachment closes. The shell stays alive, and previously accepted input remains queued and may still run. Reconnect to inspect the shell before retrying; do not automatically resend input. Reconnecting replays recent output, never input. If the shell ends before pending input reaches its PTY, its normal `exit` control includes `\"input_lost\":true` alongside the actual exit `code`.\n\nTwo refusals are worth handling, and both are a `409` on the upgrade rather than a socket that closes. A computer last started before interactive terminals existed has no channel for one in its hardware and has to be stopped and started to get one — a restart will not do it, since that is a reset of the same QEMU. And unlike the rest of this API a suspended computer is NOT resumed for you here; the refusal carries `resume_required: true`, so start it and reconnect.\n\nA third is worth retrying rather than handling: `503` with `reason: \"contention\"` means another terminal on this computer is part way through its first-time setup, which takes a few seconds and clears itself. It is deliberately not the `502` that says the guest’s terminal broker could not be reached at all — that one is worth reporting, and retrying it hard will not help.\n\nA second attach to one session detaches the first, and the displaced connection is TOLD: it receives `{\"type\":\"detached\",\"reason\":\"...\"}` and then a websocket close with code `1000`, so a client can tell \"another connection took this session\" from a dropped network. Reconnecting reattaches and displaces whoever took it, which is the mechanism for reclaiming a session from a half-dead connection you cannot close.\n\nAt most 8 sessions exist on one computer, detached ones included, since a session lasts until its shell exits. A connection to a new session name past that is refused with a `409` on the upgrade, with no `reason`: waiting does not free a session. Type `exit` in one of them to free its slot. Reattaching to a session name that already exists is never refused for this. A new name that loses a race for the last slot can still be answered after the upgrade, with `{\"type\":\"error\",\"message\":\"too many terminal sessions on this computer\"}` and a close.\n\nSeparately, at most 16 connections can be attached to one computer at once. A connection to a new session name past that is refused with a `409` on the upgrade, with no `reason`: retrying does not help until one of those connections closes. Reattaching to a session name that is already attached still displaces it as described above, so it is never refused for this. A connection being closed — displaced, or after its session ended — counts toward the 16 until it has closed, and when a new connection needs its room it is closed at once, without its `detached`, `exit` or `error` message.\n\nA refusal that comes with a JSON body, such as the `409`s and `503` above, also carries a server-generated `X-Request-ID` header, repeated in the body as `request_id`."},"events_url":{"type":"string","description":"Use this exact URL, including its origin and embedded controlling desktop credential, in a WebSocket client. A REST Bearer API key does not authenticate this socket; Bearer-only requests receive JSON 400 guidance naming events_url, equally for unknown IDs. Watch-only credentials cannot open it.\n\nA websocket that streams what this computer does, without being asked — so an agent can wait for something to happen instead of paying for a screenshot to find out that nothing has. Text frames of JSON, one event each; nothing is ever sent to it.\n\nThe first frame is `{\"type\":\"hello\", ...}` and lists the event types **this computer** can emit — not everything the platform knows how to emit. A guest with nowhere to run a watcher (a Linux one whose hardware carries no terminal channel) never produces the guest-reported half, and `events` says so rather than leaving you waiting for something that cannot arrive. The two halves of that are not the same requirement: `file.changed` needs only the terminal channel, so it is available on every Linux computer that has one — including images too old to carry the X bindings, which emit no window events at all. A guest that turns out not to be able to run one — an image built without the X bindings the watcher needs — is not always known at that first frame, and when the answer arrives afterwards a `{\"type\":\"capabilities\", \"events\": [...], \"detail\": \"...\"}` frame revises the list. It goes both ways: a computer stopped and started under an open socket can ACQUIRE the channel its watcher runs over, and the same frame says so. Treat it as replacing what `hello` advertised: the rest of the stream carries on. Ignore any frame whose `type` you do not recognise; the vocabulary grows.\n\nNominate a tree with `&watch=` and `hello` carries `watching`: the paths this stream will report file changes under, as this host has NORMALISED them. A trailing slash and a `.` segment are accepted and cleaned away, and the cleaned form is what every `file.changed` carries in `watch` — so match on what `hello` gives back rather than on what you sent. The field is absent when you nominated nothing, which means no `file.changed` can arrive at all; a path this host cannot honour is a `400` on the upgrade rather than a socket that opens and then says nothing.\n\nEach entry is `{path, armed}` — `path` as this host normalised it, and `armed` for whether that tree is ALREADY being watched. `\"armed\": false` means not live yet: wait for its `{watch, armed: true}` before reading silence as \"nothing has changed\". `\"armed\": true` means live now, and no event is coming to tell you so — somebody else nominated it first, the guest answers a nomination once, and this field is how you are told. The same split as `ready` above: state in `hello`, transitions on the stream.\n\nAt most 8 of these streams can be open on one computer at once, and one account can hold at most 128 across its computers that share a server. A connection past either is refused with a `409` on the upgrade, with no `reason` and a message naming the limit: the newest is refused and no open stream is closed to make room, so waiting does not help until one of yours closes.\n\nA computer watches at most 32 distinct trees across every stream open on it. A nomination that would take it past that is a `409` carrying `reason: \"unavailable\"` and naming the limit; close another stream, or nominate paths it is already watching, which cost nothing extra.\n\nConnect without `&since=` and `hello` also carries `windows` — the desktop as this host last saw it, each entry the same shape `GET /computers/{id}/windows` returns. It is what you are joining rather than what has just happened, so a client attached to a machine somebody else is already using starts with the screen it is looking at, and every `window.closed` after it names something you have been told about. The field is present and empty when nothing is open, and ABSENT when you resume from a cursor that could be honoured, because you already hold those windows — absent and empty are different answers, so test for the field rather than for its length. A gap counts as no continuity, so a gapped reconnect carries it too.\n\nIt rides in `hello` rather than arriving as a frame after it so that it cannot come apart from the cursor that implies it: a client which stored the cursor and dropped before a second frame would resume with continuity it had, over a desktop it did not.\n\nLast SEEN, not guaranteed live. A window whose close happened while this host had lost its link to the guest is reported into a dead pipe and stays in this picture, so an entry here can name a window that is already gone. `GET /computers/{id}/windows` asks the machine and is the authority on the present; this is what makes a later `window.closed` correlatable, and what to reconcile against that listing if it matters.\n\n`hello` also carries `ready`, and it is the difference between waiting and waiting forever. `computer.ready` is announced once per desktop SESSION, so if the desktop was already up when you attached — somebody else got there first, or you reconnected — the event has happened for that session and will not happen again for it. `\"ready\": true` means it already has; wait for the event only when it is false.\n\nPer session, not per running period, and the difference is a real one: restarting the display manager inside a guest destroys the desktop and brings up a new one without the computer ever leaving `running`. That is a new session, so `computer.ready` fires again and the windows you were told about belong to the desktop that is gone. Treat a second `computer.ready` as what it is — a desktop you have not seen before — rather than as a duplicate to discard.\n\nMind the ORDER when you do, because the replacement announces itself before it says it is ready: the new desktop’s windows arrive as `window.opened` FIRST, and `computer.ready` comes after them. The windows of the desktop that is gone are not closed one by one — there is nothing left to report their closes — so a client that empties its map when the second `computer.ready` arrives throws away the openings it was just handed, and is left describing an empty screen that has windows on it.\n\nThe stream does not mark where the replacement begins. Nothing between the last event of the old desktop and the first `window.opened` of the new one distinguishes them, so there is no rule over the openings you have already received that separates the two — an opening a moment before the replacement and an opening a moment after it look the same on the wire. Do not try to sort them. When a second `computer.ready` tells you the desktop was replaced, ask `GET /computers/{id}/windows`: it asks the machine, and it is the authority on the present here as everywhere else on this stream. Keep serving your existing map until the answer comes back — it is stale in a known way, which is better than empty.\n\nAfter that each event carries `type`, `at`, `computer`, `seq`, `cursor`, a `source` of `daemon` or `guest`, and a `data` object whose shape is the type’s own:\n\n- `window.opened` — a window EXISTS, which is not always the same as it having just appeared: a watcher starting on a desktop announces what is already open, so the first client to connect after a computer boots is told about the panel and the desktop as openings. What is already there when YOU attach comes in `hello`’s `windows` instead. The listing stays the authority on the present — it asks the guest — and this stream is the authority on changes.\n- `window.opened` / `window.focused` — `data` is a window, the same shape `GET /computers/{id}/windows` returns, with the same fields, and the same meanings for all but one. Its position and size are as they were **at that event**: moving or resizing a window does not itself produce an event, so read the listing if you need a window’s geometry right now.\n  The exception is `type`, which this stream reports more LITERALLY than the listing does — in two ways, both set out in the field’s own description. As everywhere else on this stream, the listing is the authority when the difference matters.\n- `window.blurred` — `data` is `{id}` and nothing else: the window that HAD the keyboard, reported when focus leaves it and **no window at all** takes it. Read that literally, because on a desktop that draws its own background — XFCE, which is what the Linux templates run — clicking the background or minimising the last window gives the keyboard to the desktop window itself, and what you get is an ordinary `window.focused` naming a window whose `type` is `desktop`. That is the common case and it is not a blur. A blur is for when the machine genuinely reports no active window, which a bare window manager and some lock screens do. Without it `window.focused` could only ever move focus and never retract it, so a desktop with nothing focused went on naming whichever window last held it. The window is otherwise unchanged, so it is named rather than described; it is still open and you will still get its `window.closed`. You are only sent this for a window you were told about, and only when something was focused — a desktop that already had nothing focused sends none.\n  It CAN reach you twice with no `window.focused` between, and that is not a bug to work around: if the link to the guest drops and comes back, the desktop is re-described to this host rather than replayed to you, so focus that returned while the link was down is not an event you were sent. Treat a blur as the idempotent statement it is — nothing has the keyboard now — rather than as a transition from a window you were told was focused. If you need to be certain which window has it, `GET /computers/{id}/windows` asks the machine.\n- `window.closed` — `data` is `{id}` and nothing else. The window is gone, so there is no position or size to report and none is invented; match the id against a window you were told about earlier — `hello`’s `windows`, or a `window.opened` or `window.focused` this connection was sent. You are only sent closes for those, so an id you do not recognise is not a close whose opening you missed.\n  Gone from the DESKTOP, with one exception that only a desktop of more than 200 windows can reach. This stream describes at most 200 windows plus the one that has the keyboard: a `window.focused` can therefore name a window you were not told about, and it carries the whole window so that you need nothing earlier to place it. When focus then moves to ANOTHER window outside those 200, you are sent `window.closed` for the previous one before the `window.focused` that names the new one — and that window is still on the screen. Below 200 windows a close means what it says; on a desktop that size, read it as \"this stream has stopped describing that window\". Nothing on this platform will tell you more: `GET /computers/{id}/windows` refuses a desktop of more than 200 windows rather than enumerate part of it, so the listing cannot settle whether that window is still there, and this reference does not pretend otherwise. Said here because the alternative was worse: a focus onto a window outside the picture used to be dropped, so on a desktop that size every subscriber was told the keyboard had stayed wherever it was when the machine was first watched.\n- `process.exited` — `data` is `{execution_id, pid, exit_code}` for an observed exit of a command started with `background: true`. Match `execution_id` to the ID returned in the 202 response: a PID can be reused, but the execution ID never resolves to a replacement command. Read daemon-only metadata at `GET /computers/{id}/executions/{executionId}` and independent guest output at `GET /computers/{id}/executions/{executionId}/output?stdout_offset=0&stderr_offset=0`. Each reader owns its offsets; neither route automatically resumes the computer. The legacy `GET /computers/{id}/exec/{pid}` consumes shared output and can address a newer command after PID reuse. The lost variant is `{execution_id, pid, lost: true}`, with no `exit_code`: the guest agent no longer knows about the command, as can happen after a guest restart. This host cannot say whether it finished, so no outcome is invented. Metadata may briefly report `lost` before the handle is removed; output is unavailable, and removed or expired IDs return `404`. The event tells a waiting caller to stop waiting. Guest output remains volatile and mutable, not suitable for passive Activities/history. This event transport is unchanged: connecting can start a guest watcher/broker even without nominated file watches, so it is not a passive history transport either.\n- `clipboard.changed` — `data` is `{selection}`, either `clipboard` or `primary`. The contents are deliberately not here; read them at `GET /computers/{id}/clipboard` if you want them.\n- `file.changed` — something changed under a directory YOU nominated. This is the one event type that never arrives unasked: pass `&watch=<absolute path>` when you connect (repeat it for up to four trees) and you are sent changes under those and nothing else. Without it, no `file.changed` can reach this socket at all. `data` is `{watch, path, kind, dir}` — `watch` is the tree you nominated, `path` is the absolute path that changed and is always inside it, `kind` is `created`, `modified` or `deleted`, and `dir` is present when the thing that changed is a directory.\n  The tree is watched all the way down, and directories created inside it are picked up as they appear. Nothing is announced about what is ALREADY in the tree when you nominate it — those are not changes, and listing the directory yourself is the answer to \"what is in there now\". A rename inside the tree arrives as a `deleted` for the old path and a `created` for the new one rather than as a move: inotify reports the two ends separately and one of them is often outside the tree, so each event is true about the path it names.\n  Writes are coalesced, which is why this is usable at all: an editor save is several kernel events and a compiler writing one object file is more, so changes to one path inside a short window arrive as one event. A file created and then written reads as `created`; one written and then removed reads as `deleted`. What you get is the truth about that path when the window closed, not a transcript of every write.\n  **Wait for `{watch, armed: true}` before you act on silence.** Arming is not instant: the nomination is accepted the moment you connect, but the guest has to be asked, and on a computer nobody has opened a terminal on this host has to install the watcher into the guest first — seconds, not milliseconds. inotify reports changes and not state, so anything that happens to the tree before the watch is armed is never reported and never will be. `armed` is what closes that: until it arrives, silence means \"not watching yet\". It arrives again after anything that re-arms the watch — a stop and a start, a guest reboot, a broker replaced — and a second one means what the first did: reporting starts HERE, so re-read the tree if what happened during the interruption matters. `computer.ready` says the same thing about a desktop session.\n  One shape carries no `path` and no `kind`, and a `lost` instead: `{watch, lost}` says the stream under that tree is incomplete, and treat any non-empty `lost` as \"my picture of this tree is wrong\". `\"flood\"` means the tree changed faster than the cap allows it to be reported — transient, so re-read the tree and keep listening; a build under a watched path costs you one of these rather than thousands of events. `\"budget\"` means the tree is bigger than the directory budget one watch gets, so part of it is not being watched at all — permanent for this watch, and the fix is a narrower path. `\"unwatchable\"` means the directory is not there yet, is not a directory, cannot be read, or is a SYMLINK — those are refused rather than followed, because inotify pins whatever the link resolved to when the watch was added and repointing it afterwards produces no event at all, so a followed link would report one tree under another tree’s name; nominate the real path. This reason recovers on its own where it can — nominating the directory a job is about to create is a supported thing to do, and the watch starts by itself when it appears. That recovery is announced by `{watch, armed: true}` and by nothing else: there is no synthetic event for the directory’s own creation.\n  Nominate the narrowest tree you can. A whole home directory under a build is thousands of changes a second, and the replay history this stream keeps is per COMPUTER and shared with every other subscriber to it — so a broad watch spends the history that a client resuming with `&since=` needs. The cap above is what stops that being unbounded, but a watch that is permanently flooding tells you very little either way.\n- `computer.ready` — the guest’s desktop session is up and accepting input. This is the one to wait for after creating a computer, in place of screenshotting until something appears.\n- `computer.idle` — nobody has touched this machine for its whole idle window. `data` carries `idle_seconds`. Listening is not using: holding this socket open is not activity, or the event could never fire — and it does not hold off an automatic suspend either, so on a computer configured to suspend when idle expect `computer.suspended` to follow. Setting `idle_suspend_min: 0` stops the suspend without stopping the event, subject to the plan’s never-suspend limit.\n- `computer.started` / `computer.stopped` / `computer.suspended` — `data` carries `status`, and `previous` where there was one. It is absent on the first transition a host reports for a computer after the daemon restarts, which has no earlier status to have moved from — so read it as optional rather than assuming a string is always there.\n\n**Reconnecting.** Every event has an opaque `cursor`, and so does the opening `hello` — that one is where the stream is at the moment you attach, to store if you disconnect before seeing an event. Pass the last one you have as `&since=<cursor>` and you get what you missed. If this host can no longer replay that far you get a `{\"type\":\"gap\"}` frame instead, naming the oldest cursor it still holds — which is your signal to reconcile with a listing rather than assume nothing happened. It carries no `seq`: a gap is a statement about the stream rather than a position in it, so a client that skips anything not newer than the last sequence it saw must not skip this. A cursor from before a daemon restart always reads as a gap, which is correct: the numbering it belongs to is gone.\n\n**When this host ends the stream** it says so first, with `{\"type\":\"closed\", \"detail\": \"...\"}`, and the sentence is the difference between a socket worth reopening at once and one that is not. A subscriber that stopped reading for long enough is put down deliberately rather than quietly skipped — dropping events into the floor would be a second, silent way to miss one — so reconnect with the last cursor you hold and you get what you missed, or an honest gap. A computer this host no longer holds says so instead, and that one is not worth retrying against the same place. A socket that simply dies carries no such frame, which is how you tell a network from a decision.\n\n`source` is worth reading. `daemon` means this platform observed it. `guest` means the machine reported it about itself — every `window.*` event, `clipboard.changed`, `file.changed` and `computer.ready` — and anyone with root inside the guest can make those say anything. They are your machine describing itself, which is exactly as much as they are worth.\n\nCarries the same controlling credential as `url`; absent for a viewer, because a window title is content and a watch-only credential must not read it. Absent on Windows guests. A suspended computer is refused with `409` and `resume_required: true`, and a stopped one with a `409` carrying `reason: \"unavailable\"`."},"clipboard":{"type":"boolean","description":"Whether this socket was provisioned with the platform-controlled parts of the guest clipboard bridge: the QEMU channel and an original golden verified to ship `spice-vdagent`. This is not current availability. A root user can install, remove, disable, or stop the agent later and this value will not change. Treat it as stale after modifying the guest; use the unconditional clipboard endpoints or your own guest-health check then. `false` for a viewer whatever the computer can do, because a watch-only connection has the clipboard taken out of it as it is negotiated (see `view_url`).\n\nEven on an unmodified guest provisioned with both halves, a paste is not guaranteed to land. The guest PULLS the selection rather than being pushed it, so the first paste after a connection is often dropped — the agent in the guest may not own the selection yet; send it again. And reading the guest’s clipboard back into the browser is `navigator.clipboard.writeText`, which needs focus and permission, needs a user gesture in Safari, and does nothing in a cross-origin frame unless `clipboard-write` was delegated to it. `GET`/`PUT /computers/{id}/clipboard` need none of that.\n\nWhen it is `false`, what to do about it depends on which half is missing, and both halves are needed. The channel is hardware and is acquired on a COLD start: stop the computer and start it again, or start one that is already stopped. Restarting a RUNNING computer will not do it — that resets the guest rather than rebuilding the machine QEMU was given — and a computer that comes back from a suspend or a snapshot keeps whatever the capture had, so it may lose the channel and need a stop and a start to get it back. The other platform-controlled half is `spice-vdagent` inside the original image: only images whose capability metadata matches their immutable content digest read `true`; a same-name build with different bytes does not. The computer keeps the image it was created from, and there is no operation that moves an existing computer onto a newer one. Installing the package yourself can make the bridge work, but does not change this provisioning signal. Windows guests never have it, whatever the hardware says. An unverified image reads `false` even where the agent is present; the clipboard endpoints are the answer there."}}},"ComputerConnect":{"allOf":[{"$ref":"#/components/schemas/Computer"},{"type":"object","properties":{"vnc":{"$ref":"#/components/schemas/Vnc"},"operation_id":{"type":"string","description":"Present only on the answer to a lifecycle call — a create, a clone, or a `PATCH` that resized — and never on a read: the operation it started, which `GET /operations/{id}` answers. A clone’s is `running` until its disk has been copied. Absent, with the call still done, in the rare case the operation could not be recorded; a client must not treat its absence as a failure of the call."}}}],"description":"A computer, plus what is needed to open its desktop. Carried by every response that is ONE computer — create, clone, single GET, PATCH — so connecting never costs a second call. Deliberately absent from the list response: a credential in every list is a credential in every log line that ever captured one."},"SecretBindings":{"type":"object","properties":{"secrets":{"type":"array","description":"The secrets this computer is bound to.","items":{"type":"object","properties":{"secret_id":{"type":"string","description":"The secret’s id."},"revision_id":{"type":"string","description":"The revision last delivered to this computer, or the latest when it was bound if none has been delivered since — so a new binding on a stopped computer names a revision that has not reached it yet. Its next start or restart delivers the latest and moves this to it, and so does a replaced value that reaches the running computer live."},"env":{"type":"string","description":"The environment variable it is published under, for a secret bound as one."},"file":{"type":"string","description":"For a secret bound as a file instead: its name under `/run/mandala-secrets/user/files`."}},"required":["secret_id","revision_id"]}},"version":{"type":"integer","description":"Moves on every change. `0` until the list is first written; saving an empty list, or moving the computer to another workspace, writes it too."}},"required":["secrets","version"]},"SnapshotClone":{"allOf":[{"$ref":"#/components/schemas/ComputerConnect"},{"type":"object","properties":{"memory_dropped":{"type":"boolean","description":"Present and `true` when a memory snapshot's session was asked for and the computer was built from the disk instead. Absent otherwise."},"memory_dropped_reason":{"type":"string","description":"Why, when `memory_dropped` is set: `secrets`, for a snapshot of a computer that held secrets, cloned without `inherit_secrets`; `bindings unrecorded`, for one taken before its bindings were recorded, which cannot be resumed with them; `capture unrecorded`, for one taken before the platform recorded which capture each snapshot is, which cannot be resumed with them either."}}}],"description":"The new computer, and whether the session you asked for came with it. A clone of a memory snapshot that could not be resumed is built from its disk and says so here rather than failing."},"CreateResult":{"oneOf":[{"$ref":"#/components/schemas/ComputerConnect","not":{"required":["start_error"]}},{"$ref":"#/components/schemas/CreateFailedToStart"}],"description":"Either the computer, or — when the guest was built and then would not boot — `{computer, start_error}`, so you learn the id of the machine you are now paying for rather than only that something failed. Check for `start_error` before assuming the desktop is up."},"CreateFailedToStart":{"type":"object","properties":{"computer":{"$ref":"#/components/schemas/ComputerConnect"},"start_error":{"type":"string","description":"Why it would not boot. The computer exists and is billable."},"operation_id":{"type":"string","description":"The create’s operation, already `failed` with `error.code: \"start_failed\"`. Here rather than on `computer`, because it is the answer’s and not the computer’s. Absent, with the call still done, in the rare case the operation could not be recorded; a client must not treat its absence as a failure of the call."}},"required":["computer","start_error"]},"SSHKey":{"type":"object","properties":{"id":{"type":"string","description":"`sshk-` and sixteen hex characters."},"name":{"type":"string","description":"Your label for it. Defaults to the comment that followed the key when you added it."},"public_key":{"type":"string","description":"The key as `<type> <base64>`, re-encoded from what you sent: no options and no comment. This is the line the platform writes into the computer."},"fingerprint":{"type":"string","description":"`SHA256:` and 43 characters, exactly as `ssh-keygen -l -f <file>` prints it."},"key_type":{"type":"string","description":"One of `ssh-ed25519`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com` or `ssh-rsa`."},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"last_used_at":{"type":["string","null"],"description":"When this key last opened a connection to a computer. `null` until it has."},"reach":{"type":"string","enum":["everywhere","this_account","another_account"],"description":"Where this key can connect, from the account this request acts on. `everywhere`: it was added from the dashboard and works on every account where you are an owner or member. `this_account`: it was added through an API key or connected app on this account and works here only. `another_account`: it was added through an API key or connected app on a different account and does not work here; which account is not said. A key is registered once, so adding it again is a `409`: to use one key on every account, remove it and add it from the dashboard; otherwise use a separate key here."}},"required":["id","name","public_key","fingerprint","key_type","created_at","last_used_at","reach"]},"ComputerSSH":{"type":"object","properties":{"computer":{"type":"string","description":"The computer this is about."},"enabled":{"type":"boolean","description":"Whether SSH is switched on for this computer. Off until somebody switches it on."},"available":{"type":["boolean","null"],"description":"Whether this computer can run SSH at all. `false` for a computer made from a template image that predates SSH — create a new computer from the current template to use it. `null` when the computer has not been asked yet; it is asked when it next starts."},"pending":{"type":"boolean","description":"Whether the computer’s hypervisor has yet to receive the current setting and key list. It is sent again automatically; a connection attempt sends it first."},"key_count":{"type":"integer","description":"How many keys may log in: every key of every owner and member of the account, except keys added through an API key or connected app on a different account, and except the keys of a person whose invitation is not yet accepted, who is suspended, or whose seat is suspended. None at all while the account itself is suspended. `0` while SSH is off."},"keys_pushed":{"type":"integer","description":"How many of those the computer is given. The same as `key_count` unless the account holds more keys than one computer accepts (200); then the keys of the members who joined most recently, and each person’s newest keys, are the ones left out, and connecting with one of them is refused."},"error":{"type":["string","null"],"description":"Set when the computer’s hypervisor refused the current setting. It is not sent again until a key or this setting changes; `pending` is `false` meanwhile. `null` otherwise."}},"required":["computer","enabled","available","pending","key_count","keys_pushed","error"]},"Ack":{"type":"object","properties":{"ok":{"type":"boolean","description":"Always `true`. A failure is a status code, not `ok: false`."}}},"LifecycleAck":{"type":"object","properties":{"ok":{"type":"boolean","description":"Always `true`. A failure is a status code, not `ok: false`."},"operation_id":{"type":"string","description":"This call’s operation, already `succeeded`: the step was done before the answer was sent. Read it with `GET /operations/{id}`. Absent, with the call still done, in the rare case the operation could not be recorded; a client must not treat its absence as a failure of the call."}},"required":["ok"]},"Operation":{"type":"object","properties":{"id":{"type":"string","description":"`op_` and 24 hex characters: the `operation_id` a lifecycle call answered with."},"kind":{"type":"string","enum":["create","clone","start","stop","suspend","restart","restore","resize","move","delete"],"description":"The call that started it. `clone` is either clone; `resize` is a `PATCH /computers/{id}` that resized; `move` is `POST /computers/{id}/move`; `delete` is `DELETE /computers/{id}`, `succeeded` once it was accepted (its answer says what was removed and what of a purge is still queued). More kinds may be added — treat one you do not know as opaque rather than as an error."},"computer_id":{"type":["string","null"],"description":"The computer it is about — for a create or a clone, the new one. `null` only for a restore whose computer could not be named when it was recorded."},"state":{"type":"string","enum":["pending","running","succeeded","failed"],"description":"`pending` (accepted, nothing of the computer’s has changed yet — a move still getting its target ready, or a call whose `Idempotency-Key` reserved it and whose answer has not arrived), `running`, `succeeded` or `failed`. The last two are final.\n\n`succeeded` means the platform finished the step this call answers for: the guest was started, the disk was copied, the move landed at the new size. It does not mean the desktop inside has finished booting."},"error":{"type":["object","null"],"properties":{"code":{"type":"string","description":"`start_failed` (a create built the computer and it would not boot), `build_failed` (a clone’s disk copy did not finish), `computer_gone` (it was deleted first), `move_failed` (nothing moved), `resize_not_applied` (the move landed and the resize did not: the computer is on another host at its OLD size), or `lost` (the platform stopped watching, or never heard how a call ended: one made with an `Idempotency-Key`, or a start, stop, suspend or restart made without one whose host did not answer in time — read the computer). More may be added."},"message":{"type":"string","description":"A sentence about it, meant for a person."}},"required":["code","message"]},"idempotency_key":{"type":["string","null"],"description":"The `Idempotency-Key` the call that started it was sent with, and `null` for a call sent without one. `GET /operations?idempotency_key=` finds it by this when asked with a credential of the same scope as that call’s, since keys are kept per scope."},"created_at":{"type":"string","description":"RFC 3339 timestamp of when the call was answered — or, for a call sent with an `Idempotency-Key`, of when it was received, since it is recorded before it is carried out."},"updated_at":{"type":"string","description":"RFC 3339 timestamp of the last change of `state` or `error`."},"finished_at":{"type":["string","null"],"description":"RFC 3339 timestamp, `null` while it is live. For a clone, when the platform saw the copy end, which can trail the end itself by up to a minute."}},"required":["id","kind","computer_id","state","error","idempotency_key","created_at","updated_at","finished_at"]},"OperationList":{"type":"object","properties":{"operations":{"type":"array","items":{"$ref":"#/components/schemas/Operation"}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` for the page after this one; `null` on the last page."}},"required":["operations","next_cursor"]},"DeleteResult":{"type":"object","properties":{"ok":{"type":"boolean","description":"Whether the requested selected-copy cleanup completed."},"snapshots_deleted":{"type":"integer","description":"Physical snapshot copies confirmed deleted. Duplicate IDs on different hosts count separately. Zero when snapshots are kept."},"computer_deleted":{"type":["boolean","null"],"description":"For fleet purge requests: true records a prior confirmed computer deletion or complete fleet absence observed during this request; it does not guarantee that the computer remains absent. false means deletion is unconfirmed or the computer was observed present again. null means the deletion outcome or current presence is unknown, including transport uncertainty or a fleet topology change."},"error":{"type":"string","description":"An explanation of a partial, queued, refused, or unknown outcome."},"purge":{"$ref":"#/components/schemas/SnapshotPurge"},"operation_id":{"type":"string","description":"This delete’s operation (kind `delete`), `succeeded` because the delete was accepted; this body, not the operation, says what was removed and what is still queued. Absent, with the call still done, in the rare case the operation could not be recorded; a client must not treat its absence as a failure of the call."}}},"SnapshotPurge":{"type":"object","properties":{"selected":{"type":"integer","description":"Physical copies selected by the confirmation."},"confirmed":{"type":"integer","description":"Selected copies confirmed absent after deletion."},"queued":{"type":"integer","description":"Selected copies still listed with a deleting intent. An accepted deletion that remains durable or pending has an unknown outcome."},"failed":{"type":"integer","description":"Selected copies whose deletion was refused."},"unknown":{"type":"integer","description":"Selected copies with an unknown outcome, including requests not sent after transport uncertainty."},"remaining":{"type":["integer","null"],"description":"Selected copies still visible in the final complete inventory; null if that inventory is unavailable."},"unselected":{"type":["integer","null"],"description":"New physical copies not selected by this request; null if the final inventory is unavailable. New consent is required to delete them."},"complete":{"type":"boolean","description":"All selected copies were confirmed absent in the final complete inventory. Does not promise that later imports cannot appear."}}},"Move":{"type":"object","properties":{"computer_id":{"type":"string","description":"The computer being moved."},"state":{"type":"string","description":"Where it has got to. `staging` and `moving` and `resizing` are live; `done`, `moved`, `failed` and `lost` are terminal.\n\nThe three terminal failures are three different things and the difference is the whole point of them being separate words. `failed` means nothing happened — the computer is where it was, at the size it was. `moved` means the computer IS on another host but at its OLD size: the move landed and the resize did not, which is recoverable by resizing it again where it now is. `lost` means we stopped watching and cannot say; read the computer to find out."},"detail":{"type":"string","description":"A sentence about the state, meant to be shown to a person. Empty while nothing has gone wrong."},"live":{"type":"boolean","description":"It is still running. This is the flag to poll on rather than comparing `state` to a list."},"cpu":{"type":"integer","description":"Present only when the move is applying a new value for it."},"ram_mb":{"type":"integer","description":"Present only when the move is applying a new value for it."},"disk_gb":{"type":"integer","description":"Present only when the move is applying a new value for it."},"started_at":{"type":"string","description":"RFC 3339 timestamp."},"finished_at":{"type":"string","description":"RFC 3339 timestamp. Absent while `live` is true."},"operation_id":{"type":"string","description":"Present only on the answer to `POST /computers/{id}/move`: the move’s operation, which follows this row to its end. `GET /operations/{id}` is the same wait as polling `GET /moves`. Absent, with the call still done, in the rare case the operation could not be recorded; a client must not treat its absence as a failure of the call."}}},"MoveList":{"type":"object","properties":{"moves":{"type":"array","items":{"$ref":"#/components/schemas/Move"}}}},"WindowActed":{"type":"object","properties":{"ok":{"type":"boolean"},"window":{"oneOf":[{"$ref":"#/components/schemas/Window","description":"The window as it now is, or `null` when the action left nothing to describe."},{"type":"null"}],"description":"The window as it now is, or `null` when the action left nothing to describe."},"gone":{"type":"boolean","description":"The window closed. This is what separates the two outcomes that have no `window` to show: `true` means it is gone, which is what a `close` is for, while `false` with no `window` means the action happened and the guest could not describe the result."}}},"Snapshot":{"type":"object","properties":{"id":{"type":"string"},"computer_id":{"type":"string","description":"The computer these bytes came from. May name a computer that no longer exists."},"computer_name":{"type":"string","description":"For a computer that still exists, its current name — so a rename is reflected without re-reading anything. For an orphan, the name at capture, which is all there is. Absent on snapshots taken before the daemon recorded it."},"orphaned":{"type":"boolean","description":"True when no host that answered lists the source computer, and every host that did not answer is one the platform’s records show holding none of this account’s computers. False when a host lists it. Omitted when that cannot be established, with `computer_unreachable: true`. Also omitted on the row `DELETE /snapshots/{id}` answers with, which does not work it out; read it from `GET /snapshots`."},"computer_unreachable":{"type":"boolean","description":"True when source computer presence cannot be established. Snapshot bytes may still be available; orphaned is omitted."},"restore_available":{"type":"boolean","description":"The snapshot has a usable copy on the current source computer host. False for old-host copies; clone remains available when the snapshot itself is reachable."},"name":{"type":"string"},"kind":{"type":"string","description":"`disk` or `memory`.","enum":["disk","memory"]},"state":{"type":"string","description":"Where these bytes have got to. `capturing` — the copy is still being taken, and this row is a placeholder rather than a snapshot: restore, clone and delete all 404 on it, though its id is already the id it will keep. `pending` — it is on the host and usable, and is being pushed to backup storage. `durable` — it is in backup storage too, which is the state your plan’s retention (`GET /retention`) ages out. `deleting` — a deletion began and has not finished, whether still under way or stalled; you only see these if you asked for them with `include=unfinished`.\n\nPoll this after a capture, and wait for it to stop reading `capturing` rather than for it to read `pending`. `pending` is where a finished capture lands, but replication can carry it on to `durable` between two polls; both are states you can act on. A row that disappears without ever leaving `capturing` is a capture that failed.\n\nA capture holds its computer until its upload to backup storage has finished, not just until it leaves `capturing`: a new capture of the SAME computer is refused (`409`, `reason: \"contention\"`) while that upload runs, which is ordinarily for as long as this row reads `pending`, and is accepted once it reads `durable`. Restore, clone and delete all work from `pending`.","enum":["capturing","pending","durable","deleting"]},"size_bytes":{"type":"integer"},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"os":{"type":"string"},"template":{"type":"string"},"cpu":{"type":"integer"},"ram_mb":{"type":"integer"},"disk_gb":{"type":"integer"},"resolution":{"type":"string","description":"The screen the capture was taken at, and what a clone of it comes up as."},"auto":{"type":"boolean","description":"Taken by the scheduler rather than by hand. Only these are ever aged out by your plan’s retention, and `GET /retention` is the window they are aged out on."},"incremental":{"type":"boolean","description":"This snapshot is a link in a chain rather than a full copy, which is why it bills smaller."},"unreachable":{"type":"boolean","description":"As on Computer — a row from the placement cache, with nothing else on it."}}},"Holdings":{"type":"object","properties":{"count":{"type":"integer","description":"Physical snapshot copies across the complete fleet, including duplicate IDs on different hosts, captures, and unfinished deletions."},"size_bytes":{"type":"integer","description":"The total currently reported bytes of those physical copies."},"computer_present":{"type":"boolean","description":"Whether the source computer is currently present. This presence is bound into the fingerprint. A snapshot-only cleanup confirmation must require false."},"capturing":{"type":"integer","description":"In-flight captures. Purge is refused while any remain."},"deleting":{"type":"integer","description":"Copies with unfinished deletion work."},"fingerprint":{"type":"string","description":"Opaque versioned fingerprint binding scope, computer presence, host lifecycle identities and the exact physical copies. Pass it back as expect on DELETE /computers/{id}?snapshots=delete; a changed inventory is refused before mutation."}}},"Template":{"type":"object","properties":{"name":{"type":"string","description":"The short name of the template, and what a computer reports as its own `template`. Accepted as `template` on a create for the templates we publish; one you published is named by its `ref`."},"ref":{"type":"string","description":"The pinned `namespace/name@version` for this template. Also accepted as `template` on a create, and the form to use when it matters that you get exactly this template and this version — a short name resolves to whatever the host currently has under it."},"label":{"type":"string","description":"Human-readable name."},"os":{"type":"string","description":"`linux`. (`windows` is not currently offered on any plan.)"},"desktop":{"type":"string","description":"The display protocol this template’s desktop speaks: `wayland`, `x11`, or absent (X11, or a host too old to say); branch on `=== \"wayland\"`. Screenshots, input and `exec` are identical either way — they are taken below the guest — but two things differ on a Wayland template. A window `id` is the compositor’s address rather than an X window id (opaque and handed back the same way, but not comparable across the two), and moving or resizing a window the compositor is TILING is refused rather than silently ignored, because a tiled window’s geometry belongs to the layout. Absent also on a host deployed before this field existed, which is why it is not defaulted to `x11` here."},"icon":{"type":"string","description":"A slug naming a mark this console has an asset for, e.g. `claude` or `debian`. Cosmetic — nothing here validates it against a list, and an unrecognised value or none just means the template has no particular icon."},"cpu":{"type":"integer","description":"The default this template builds at."},"ram_mb":{"type":"integer"},"disk_gb":{"type":"integer","description":"The FLOOR this template builds at, not a default like the two above — a create asking for less is raised to this and charged at it."}}},"TemplateValidation":{"type":"object","properties":{"valid":{"type":"boolean","description":"Whether the DOCUMENT is well formed — the schema and the rules that read the file itself. It is not a publish preflight: a valid document is still refused by `POST /templates` for things only your account decides, and none of them are visible here — a namespace that is not yours, a plan that does not carry publishing, a ref already taken, the per-account ceiling, an `os` that contradicts the `family` the document names, a `spec.hardware.disk_gb` below the disk floor of the family or of the `spec.from` parent (400), and a document with `spec.build` steps or `spec.env` naming a family that is not yours (403)."},"problems":{"type":"array","items":{"type":"string"},"description":"Present when `valid` is false. A document that could not be PARSED has exactly one entry, because nothing after the syntax error was read. A document that parsed is reported with every problem one pass can reach rather than the first, so a file with four mistakes in four fields takes one call — with one exception worth knowing: a build step that is structurally wrong (it does nothing, or carries more than one operation) is reported as that, and the problems INSIDE it are not looked at until you have fixed it. Each names what is wrong in the author’s own vocabulary."},"ref":{"type":"string","description":"`namespace/name@version`, as the document’s metadata spells it."},"doc_digest":{"type":"string","description":"Identifies the document, as `sha256:…`. Taken over `canonical` below — so it changes with anything that changes what the document MEANS, a label included, and NOT with comments, key order, indentation or YAML-versus-JSON, none of which survive parsing. Being over `canonical` is also what lets you check it yourself."},"template":{"$ref":"#/components/schemas/Template","description":"The catalogue row this document describes — the same shape `GET /templates` lists."},"canonical":{"type":"string","description":"The document as the digests were taken over it, with key order and whitespace normalised. Two files differing only in comments and key order are the same document and hash the same."},"build_digest":{"type":"string","description":"Covers only what decides the image, as `sha256:…` — so a new label or a version bump leaves it alone. Present only for a document with no `spec.from`, which builds nothing (`spec.build` and `spec.env` need a parent), so it cannot tell you whether an edit to a document that builds means a rebuild. Replaced by `build_digest_needs` on a document with a parent."},"build_digest_needs":{"type":"string","description":"Replaces `build_digest` on a document that names a parent in `spec.from`: a sentence saying what could not be computed. A build digest needs an identity for the CONTENTS of the base image, which is a fact about a host holding it rather than anything in your document."}},"required":["valid"]},"Size":{"type":"object","properties":{"id":{"type":"string","description":"What to pass as `size` on `POST /computers`."},"label":{"type":"string","description":"Human-readable name."},"template":{"type":"string","description":"The template this row builds — it comes with the size, so do not send `template` alongside."},"cpu":{"type":"integer"},"ram_mb":{"type":"integer"},"disk_gb":{"type":"integer"},"allowed":{"type":"boolean","description":"Whether your plan’s per-computer ceilings admit this size. What the account already holds is not counted here — a create can still be refused against the plan’s pools, with the refusal naming the pool."},"cheapest_plan":{"type":["string","null"],"description":"The id of the cheapest plan whose ceilings admit this size — the plan to name when `allowed` is false. Null if no purchasable plan admits it."}}},"Schedule":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether a capture is taken in this window. A computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot."},"hour":{"type":"integer","description":"0–23, in `tz`."},"minute":{"type":"integer","description":"0–59."},"tz":{"type":"string","description":"IANA zone name. `UTC` when unset."}},"required":["enabled","hour","minute","tz"]},"Retention":{"type":"object","properties":{"daily":{"type":"integer","description":"How many of the last days keep their newest automatic snapshot."},"weekly":{"type":"integer","description":"The same, over ISO weeks."},"monthly":{"type":"integer","description":"The same, over calendar months."}},"required":["daily","weekly","monthly"]},"GuestDirectoryEntry":{"type":"object","properties":{"name":{"type":"string","description":"Exact filename. Preserve its Unicode, spaces and punctuation when constructing a path."},"type":{"type":"string","enum":["file","directory","symlink","special","unavailable"]},"size_bytes":{"type":"integer","description":"Size of a regular file, when known and representable as a safe JSON integer. Absent for other types."}},"required":["name","type"]},"GuestDirectory":{"type":"object","properties":{"path":{"type":"string","description":"The requested directory path."},"entries":{"type":"array","items":{"$ref":"#/components/schemas/GuestDirectoryEntry"}},"truncated":{"type":"boolean","description":"True when the directory exceeded the bounded listing. Entries are a partial, unordered sample, not a complete listing."},"skipped":{"type":"integer","description":"Number of inspected names omitted because their encoding or control characters cannot be used by this file API."}},"required":["path","entries","truncated","skipped"]},"FileUpload":{"type":"object","properties":{"path":{"type":"string","description":"Destination inside the guest."},"upload_id":{"type":"string","description":"Client-generated 32 lowercase hex characters identifying this upload."},"offset":{"type":"integer","description":"Bytes staged. Resume from this offset; do not guess after a lost response."},"size":{"type":"integer","description":"Declared total file size, at most 2147483648 bytes (2 GiB)."},"complete":{"type":"boolean","description":"The complete file has been published at path."}},"required":["path","upload_id","offset","size","complete"]},"FileWrite":{"type":"object","properties":{"path":{"type":"string","description":"Where it landed inside the guest."},"bytes":{"type":"integer","description":"What the guest agent acknowledged writing, not the length of what you sent. The daemon refuses a short write, so a 200 with a smaller number than you sent has told you something about your own request body."}}},"WindowList":{"type":"object","properties":{"windows":{"type":"array","items":{"$ref":"#/components/schemas/Window"}}}},"Clipboard":{"type":"object","properties":{"text":{"type":"string","description":"What is on the desktop’s CLIPBOARD selection right now — the one Ctrl-C writes to, not the X PRIMARY selection that middle-click pastes.\n\nAn empty desktop clipboard is `\"\"`, and it is not distinguishable from one holding an empty string, because X does not distinguish them either."}}},"Window":{"type":"object","properties":{"id":{"type":"string","description":"The window id, for `POST /computers/{id}/windows/{window}`."},"title":{"type":"string"},"class":{"type":"string"},"type":{"type":"string","description":"The window type. On an X11 desktop this is the EWMH `_NET_WM_WINDOW_TYPE`, lower-cased with the `_NET_WM_WINDOW_TYPE_` prefix dropped: `normal`, `dialog`, `dock`, `desktop`. A window that declares only a VENDOR type is reported as that atom, lower-cased and prefix intact, so do not assume the value comes from that list. It is EMPTY when the window declares no type at all, which is legal — EWMH treats an untyped managed window as normal, and this reports what the window said rather than filling it in. The listing has one exception to that, below.\n\nOn a WAYLAND desktop it is `normal` for every window, and nothing below applies. An EWMH window type is an X11 protocol; a Wayland toplevel does not declare one, so there is nothing to read and nothing is inferred from anything else.\n\n`type` is also the field where `GET /computers/{id}/windows` and the event stream can report a window differently. Two ways, and the same shape both times: the LISTING interprets what it reads, the STREAM reports what the window literally declared. Both are X11 only:\n\n1. The listing reports `dialog` for an untyped window that names another as its parent (`WM_TRANSIENT_FOR`) — a toolkit too old to set a type still marks its modals that way, and “which of these is the dialog in front” is one of the questions the listing exists to answer. The guest watcher does not read that property, so the same legacy modal is `dialog` in the listing and empty on the stream.\n2. A window may declare SEVERAL types — a vendor one first, a standard one after it as the fallback. The listing walks that list and reports the standard atom; the watcher forwards only the first. So a window typed `_COMPIZ_WINDOW_TYPE, _NET_WM_WINDOW_TYPE_NORMAL` is `normal` in the listing and `_compiz_window_type` on the stream.\n\nThis applies to everything the watcher describes: `window.opened`, `window.focused`, and the `windows` that `hello` carries, which are one picture. The other fields of this object mean the same thing on both paths, so a difference in one of THOSE is only ever a difference in when each was read. If the type matters to you, ask the listing — it is the authority on the present here as it is elsewhere."},"pid":{"type":"integer","description":"The process inside the guest that owns this window, where the window says so. ABSENT rather than `0` when it does not: a guest is free to advertise `_NET_WM_PID` 0, and reporting that as “no pid” would be inventing an answer.\n\nIt does not identify the window. An application that keeps one process for several windows — `xfce4-terminal` is one — reports the same pid on all of them, so killing this pid can take windows you never asked about."},"x":{"type":"integer"},"y":{"type":"integer"},"width":{"type":"integer"},"height":{"type":"integer"},"focused":{"type":"boolean"},"visible":{"type":"boolean","description":"False for a minimised window, and this is the only way to tell one from a window on the screen. A minimised window stays on the list and keeps the coordinates it had, so an agent that clicks at them is clicking at whatever is actually there. Nothing else in the row says so — the title, the class, the geometry and the `focused` flag all read as ordinary."}},"required":["id","title","class","type","x","y","width","height","focused","visible"]},"OpenAIInput":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","enum":["click","double_click","drag","move","scroll","keypress","type","wait","screenshot"],"description":"OpenAI action discriminator; mutually exclusive with action."},"x":{"type":"number","description":"Required for pointer actions except drag. Absolute pixels in the screen space `resolution` reports; rescale when a screenshot’s `X-GC-Capture-Size` differs."},"y":{"type":"number","description":"Required for pointer actions except drag. Absolute pixels in the screen space `resolution` reports; rescale when a screenshot’s `X-GC-Capture-Size` differs."},"button":{"type":"string","enum":["left","right","wheel","back","forward"],"description":"Required for click only."},"keys":{"type":["array","null"],"items":{"type":"string"},"maxItems":16,"description":"A keypress chord, or keys held during a pointer gesture."},"text":{"type":"string","description":"Required for type; existing Unicode typing limits apply."},"path":{"type":"array","minItems":2,"maxItems":128,"items":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"}},"required":["x","y"],"additionalProperties":false},"description":"Required for drag: ordered {x,y} waypoints."},"scroll_x":{"type":"number","description":"Required for scroll: horizontal pixel delta, ±5000."},"scroll_y":{"type":"number","description":"Required for scroll: vertical pixel delta, ±5000."}}},"InputBatchResult":{"type":"object","properties":{"ok":{"type":"boolean","description":"Whether every action completed."},"completed":{"type":"integer","description":"Number of completed actions, in order. Not a rollback guarantee."},"results":{"type":"array","items":{"$ref":"#/components/schemas/InputResult"},"description":"One result for each completed action."},"failed_index":{"type":"integer","description":"Zero-based index of the failed action, only on execution failure. That action may have partially applied."},"error":{"type":"string","description":"Execution failure; inspect the desktop before retrying."},"context":{"$ref":"#/components/schemas/InputContext"},"context_error":{"type":"string","description":"Optional context-read failure after an otherwise successful batch."}},"required":["ok","completed","results"]},"InputResult":{"type":"object","properties":{"ok":{"type":"boolean","description":"The action was carried out."},"mechanism":{"type":"string","enum":["physical","unicode","mixed"],"description":"Present on successful `type`: `physical` for plain ASCII typed as US-layout key events; `unicode` for text with no ASCII, typed by GTK Unicode composition; `mixed` for both, typed in request order by the same guest helper, which pressed the ASCII key positions itself. Confirms dispatch; verify that the target accepted the exact text."},"x":{"type":"integer","description":"Where the pointer is, for `cursor_position`. Absent on every other action."},"y":{"type":"integer","description":"The same."},"known":{"type":"boolean","description":"Read this BEFORE `x` and `y`. QEMU’s tablet is an absolute pointing device: it accepts coordinates and reports none back, so the only position this platform can tell you about is one it put the pointer at itself. `false` means nothing has — a guest that has just booted, or been restored, has its pointer wherever it left it — and `x` and `y` are then `0`, which is a corner the pointer is probably not in rather than a reading. Absent on every action but `cursor_position`."},"context":{"$ref":"#/components/schemas/InputContext","description":"The desktop just after the action, when the request asked with `?context=1` and its windows could be read. Absent otherwise."},"context_error":{"type":"string","description":"What `?context=1` could not read. With no `context`, it says why the windows could not be listed: the sentence `GET /computers/{id}/windows` would have answered (a Windows guest, no desktop session, a guest agent not answering), or that the guest did not list its windows within 3 seconds. Beside a `context`, it says why that context has no `dom`: no window has focus, the focused window is not Chromium, Chromium is not listening on its debugging port, the focused Chromium is not the desktop’s own profile or no tab of it can be tied to the focused window, its page does not fill its window (a side panel or DevTools docked beside it), the computer’s image predates page elements, or the page was not described within the same 3 seconds. Either way the action itself was carried out — `ok` is still `true` — so do not send it again; list the windows or take a screenshot instead."}},"required":["ok"]},"InputContext":{"type":"object","properties":{"windows":{"type":"array","items":{"$ref":"#/components/schemas/Window"},"description":"The windows `GET /computers/{id}/windows` lists by default — applications, without the desktop’s own panels and wallpaper — read once, straight after the action. There is no settle wait, so a window still opening when the read ran is not listed yet. `[]` for an empty desktop."},"focused":{"oneOf":[{"$ref":"#/components/schemas/Window","description":"The window in `windows` that has focus, or `null` when none of them does — including when focus is on the desktop itself, which `windows` leaves out."},{"type":"null"}],"description":"The window in `windows` that has focus, or `null` when none of them does — including when focus is on the desktop itself, which `windows` leaves out."},"dom":{"$ref":"#/components/schemas/PageContext","description":"The page on screen, when `focused` is Chromium (window class `Chromium`) and the page could be read within the 3 seconds `?context=1` has in all. Absent otherwise, with `context_error` saying why. Chromium is the default browser; Firefox, also installed, has no `dom`. Read through Chromium’s DevTools port, which it opens on the computer’s loopback interface only, on images built with it. Only the desktop’s own Chromium profile is read: a page is described only when it is the tab with focus, or the one tab on screen the focused window is named after, so a Chromium started with its own profile has no `dom`."}},"required":["windows","focused"]},"PageContext":{"type":"object","properties":{"url":{"type":"string","description":"The page’s address, at most 2048 characters."},"title":{"type":"string","description":"The page’s title, at most 300 characters."},"elements":{"type":"array","items":{"$ref":"#/components/schemas/PageElement"},"description":"The interactive elements visible in the viewport — links, buttons, form fields, `summary`, elements with a button, link, tab, menu item, checkbox, radio or textbox `role`, and `contenteditable` or `onclick` elements — that are not hidden or covered at their centre, in document order. At most 150. The top document only: nothing inside an iframe or a shadow root. `[]` when none is visible."},"truncated":{"type":"boolean","description":"The page had more interactive elements than `elements` holds."}},"required":["url","title","elements","truncated"]},"PageElement":{"type":"object","properties":{"tag":{"type":"string","description":"The element’s lower-case tag name: `a`, `button`, `input`."},"role":{"type":"string","description":"Its `role` attribute, or `\"\"`."},"name":{"type":"string","description":"The first of its `aria-label`, `title`, `placeholder`, `alt` and `name`, or `\"\"`. At most 100 characters."},"text":{"type":"string","description":"What it says: its text, or a field’s value. A password field’s value is never read, and a checkbox or radio has none. At most 100 characters."},"href":{"type":"string","description":"A link’s resolved target, at most 2048 characters. Present on links only."},"x":{"type":"integer","description":"The left edge of the element’s visible part, in screen pixels — the coordinates input actions take. Click at `x + width/2`, `y + height/2`. Placed from the Chromium window’s position, with the page filling the window below its toolbar. A page narrowed by a side panel or DevTools docked beside it is refused (no `dom`, and `context_error` says why); DevTools docked at the bottom is not detected and moves the page, not these."},"y":{"type":"integer","description":"The top edge of the visible part, in screen pixels."},"width":{"type":"integer","description":"The visible part’s width, in screen pixels; at least 1."},"height":{"type":"integer","description":"The visible part’s height, in screen pixels; at least 1."}},"required":["tag","role","name","text","x","y","width","height"]},"Exec":{"type":"object","properties":{"result_id":{"type":"string","pattern":"^res_[a-f0-9]{32}$","description":"Optional committed synchronous retained version, present only after explicit retain_output capture succeeds. Never an execution ID or replay key."},"exit_code":{"type":"integer"},"stdout_b64":{"type":"string","description":"What the command printed on stdout, base64. Decode it: `echo \"$STDOUT_B64\" | base64 -d`, or your language’s own decoder.\n\nBase64 rather than text because a JSON string is UTF-8 by definition and a command’s output is bytes. A field carrying the bytes directly could only carry the ones that happen to be valid UTF-8, and everything else — a tarball, a PNG, a latin-1 build log — arrived silently rewritten, byte by byte, into U+FFFD. There is no flag for that and there could not be a useful one: half a response would be intact and half replaced, with nothing to say which."},"stderr_b64":{"type":"string","description":"The same, for stderr."},"timed_out":{"type":"boolean","description":"The command hit `timeout_s` and was not waited for any longer. Check this BEFORE reading `exit_code` — a timed-out command has not reported one, and `exit_code` then reads `-1`, a sentinel and not the command’s status."},"out_truncated":{"type":"boolean","description":"The guest agent stopped capturing stdout before the command stopped producing it, at 16 MiB. What you have is a prefix, and nothing else in the response would tell you so."},"err_truncated":{"type":"boolean","description":"The same, for stderr."}},"required":["exit_code","stdout_b64","stderr_b64","timed_out","out_truncated","err_truncated"]},"ExecHandle":{"type":"object","properties":{"execution_id":{"type":"string","description":"Opaque stable identity for the execution routes. Never use a PID to identify stored work."},"pid":{"type":"integer","description":"The guest pid. This is the `{pid}` path segment on the two follow-up routes."},"command":{"type":"string","description":"The command line, echoed back."},"running":{"type":"boolean"},"exited":{"type":"boolean","description":"False on a handle. The command has just been started; this is `ExecStatus`’s question."},"stdout_b64":{"type":"string","description":"Empty. Nothing has been read yet — the output arrives through `GET /computers/{id}/exec/{pid}`, base64 there for the reason `Exec` gives."},"stderr_b64":{"type":"string","description":"Empty, for the same reason."},"stdout_offset":{"type":"integer","description":"`0`, the point the first read starts from."},"stderr_offset":{"type":"integer","description":"`0`."},"started_at":{"type":"string","description":"RFC 3339 timestamp."}},"required":["execution_id","pid","command","running","exited","stdout_b64","stderr_b64","stdout_offset","stderr_offset","started_at"]},"ExecStatus":{"type":"object","properties":{"execution_id":{"type":"string","description":"The stable ID originally returned by background exec."},"pid":{"type":"integer"},"command":{"type":"string","description":"The command line, echoed back."},"running":{"type":"boolean"},"exited":{"type":"boolean"},"exit_code":{"type":"integer","description":"Absent until it has exited — absent rather than `0`, which is the one value worth trusting."},"stdout_b64":{"type":"string","description":"What it has printed since your previous read, base64. This read consumes it.\n\nAlways base64, on every response, for the reason `Exec` gives — and here there is a second one. A poll stops at a 1 MiB ceiling, and that cut lands on a byte offset: a multi-byte character sitting across it would have had BOTH of its halves replaced. Ordinary text output was being corrupted at every megabyte, not only binary output. Decoded, the two halves join back into the character they came from."},"stderr_b64":{"type":"string"},"stdout_offset":{"type":"integer","description":"How far the daemon has now read. Reported, not a parameter — see the note above."},"stderr_offset":{"type":"integer"},"more":{"type":"boolean","description":"There is further output waiting right now. This is the flag to poll on.\n\nABSENT rather than `false` when there is not, so `false` is a value this field never carries: code waiting for `more === false` waits for ever. Polling is safe either way — `!more` reads the same for absent and false — and no schema here marks it required, so an ordinary client simply sees `undefined`. What it is not safe to do is declare it a required boolean and put a strict runtime validator behind that, which rejects the ordinary response rather than the unusual one."},"killed":{"type":"boolean","description":"A `DELETE /computers/{id}/exec/{pid}` reached this command while the platform still had it running. Absent rather than `false` otherwise, on the same terms as `more`.\n\nThat is weaker than “it did not finish on its own”, and the difference is a real one rather than a caution. Whether a command has exited is noticed by a poll, so a command that ended a moment before your DELETE arrived is still running as far as this platform knows: the stop is carried out against a process group that has already gone, which succeeds, and the flag is set. So `killed` tells you what YOU did, not how the command ended. `exit_code` is what says that, and a command stopped by a signal reports the code its shell gives for one."},"started_at":{"type":"string","description":"RFC 3339 timestamp."}},"required":["execution_id","pid","command","running","exited","stdout_b64","stderr_b64","stdout_offset","stderr_offset","started_at"]},"Execution":{"type":"object","properties":{"execution_id":{"type":"string","description":"Opaque background execution ID. An identifier, never authorization."},"computer_id":{"type":"string","description":"The computer this execution belongs to."},"pid":{"type":"integer","description":"Guest PID, which may be reused; identify results by execution_id."},"status":{"type":"string","enum":["running","lost","exited"],"description":"Last daemon observation. Running does not imply the computer is awake; lost has no known exit code."},"started_at":{"type":"string","description":"Daemon acceptance time, RFC 3339."},"ended_at":{"type":"string","description":"Observed exit time, RFC 3339; absent unless exited."},"exit_code":{"type":"integer","description":"Present only for an observed exit. This is not task success."},"output_source":{"type":"string","enum":["volatile_guest_files"],"description":"Guest output is mutable and ephemeral, not retained history."}},"required":["execution_id","computer_id","pid","status","started_at","output_source"]},"Artifact":{"type":"object","properties":{"artifact_id":{"type":"string","pattern":"^art_[a-f0-9]{32}$"},"kind":{"type":"string","const":"artifact"},"state":{"type":"string","const":"ready"},"computer_id":{"type":"string"},"workspace_id":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"size":{"type":"integer","minimum":0,"maximum":67108864},"sha256":{"type":"string","pattern":"^[a-f0-9]{64}$"},"execution_association":{"oneOf":[{"type":"null"},{"type":"object","properties":{"kind":{"type":"string","const":"caller_selected"},"execution_id":{"type":"string","pattern":"^exec_[a-f0-9]{32}$"},"verified_at":{"type":"string","format":"date-time"}},"required":["kind","execution_id","verified_at"],"additionalProperties":false}]}},"required":["artifact_id","kind","state","computer_id","workspace_id","created_at","expires_at","size","sha256","execution_association"],"additionalProperties":false},"RetainedPrefix":{"type":"object","properties":{"bytes":{"type":"integer","minimum":0,"maximum":4194304},"sha256":{"type":"string","pattern":"^[a-f0-9]{64}$"},"source_offset":{"type":"integer","const":0},"next_source_offset":{"type":"integer","minimum":0,"maximum":4194304},"end_reason":{"type":"string","enum":["observed_eof","byte_limit"],"description":"Why this immutable prefix stopped. Neither value proves execution completion."}},"required":["bytes","sha256","source_offset","next_source_offset","end_reason"]},"RetainedResult":{"type":"object","properties":{"version":{"type":"integer","const":1},"result_id":{"type":"string","pattern":"^res_[a-f0-9]{32}$"},"kind":{"type":"string","const":"background-output"},"state":{"type":"string","const":"ready","description":"Bytes were published, not a statement of task success."},"account_id":{"type":"string"},"computer_id":{"type":"string"},"workspace_id":{"type":["string","null"]},"execution_id":{"type":"string","pattern":"^exec_[a-f0-9]{32}$"},"capture_started_at":{"type":"string","format":"date-time"},"captured_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"source":{"type":"string","const":"volatile_guest_files"},"execution_observation":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","const":"running"},"observed_at":{"type":"string","format":"date-time"}},"required":["status","observed_at"]},{"type":"object","properties":{"status":{"type":"string","const":"exited"},"observed_at":{"type":"string","format":"date-time"},"exit_code":{"type":"integer","minimum":-2147483648,"maximum":2147483647}},"required":["status","observed_at","exit_code"]}]},"stdout":{"$ref":"#/components/schemas/RetainedPrefix"},"stderr":{"$ref":"#/components/schemas/RetainedPrefix"},"diagnostic":{"type":"object","properties":{"bytes":{"type":"integer","minimum":0,"maximum":65536},"sha256":{"type":"string","pattern":"^[a-f0-9]{64}$"},"source":{"type":"string","const":"wrapper"},"diagnostic_truncated":{"type":"boolean"}},"required":["bytes","sha256","source","diagnostic_truncated"]}},"required":["version","result_id","kind","state","account_id","computer_id","workspace_id","execution_id","capture_started_at","captured_at","expires_at","source","execution_observation","stdout","stderr","diagnostic"]},"RetainOutput":{"oneOf":[{"type":"boolean"},{"type":"object","properties":{"max_bytes_per_stream":{"type":"integer","minimum":1,"maximum":4194304,"default":1048576},"retention_seconds":{"type":"integer","minimum":1,"maximum":604800,"default":86400}},"additionalProperties":false}],"description":"Explicit opt-in, at most 512 UTF-8 bytes. Missing/false preserves legacy behavior. True uses defaults."},"SynchronousRetainedPrefix":{"type":"object","properties":{"bytes":{"type":"integer","minimum":0,"maximum":4194304},"sha256":{"type":"string","pattern":"^[a-f0-9]{64}$"},"source_offset":{"type":"integer","const":0},"next_source_offset":{"type":"integer","minimum":0,"maximum":4194304},"source_response_bytes":{"type":"integer","minimum":0,"maximum":16777216},"end_reason":{"type":"string","enum":["response_end","byte_limit"]},"upstream_truncated":{"type":"boolean"}},"required":["bytes","sha256","source_offset","next_source_offset","source_response_bytes","end_reason","upstream_truncated"],"additionalProperties":false},"SynchronousRetainedResult":{"type":"object","properties":{"version":{"type":"integer","const":1},"result_id":{"type":"string","pattern":"^res_[a-f0-9]{32}$"},"kind":{"type":"string","const":"synchronous-output"},"state":{"type":"string","const":"ready"},"account_id":{"type":"string"},"computer_id":{"type":"string"},"workspace_id":{"type":["string","null"]},"execution_id":{"type":"null"},"source":{"type":"string","const":"exec_response"},"capture_started_at":{"type":"string","format":"date-time"},"captured_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"execution_observation":{"type":"object","properties":{"status":{"type":"string","const":"exited"},"observed_at":{"type":"string","format":"date-time"},"exit_code":{"type":"integer","minimum":-2147483648,"maximum":2147483647}},"required":["status","observed_at","exit_code"],"additionalProperties":false},"stdout":{"$ref":"#/components/schemas/SynchronousRetainedPrefix"},"stderr":{"$ref":"#/components/schemas/SynchronousRetainedPrefix"},"diagnostic":{"type":"null"}},"required":["version","result_id","kind","state","account_id","computer_id","workspace_id","execution_id","source","capture_started_at","captured_at","expires_at","execution_observation","stdout","stderr","diagnostic"],"additionalProperties":false},"OutputRetainedResult":{"oneOf":[{"$ref":"#/components/schemas/RetainedResult"},{"$ref":"#/components/schemas/SynchronousRetainedResult"}]},"ExecutionOutput":{"type":"object","properties":{"execution_id":{"type":"string","description":"The requested stable execution ID."},"stdout_b64":{"type":"string","description":"Raw stdout bytes, base64; at most limit decoded bytes."},"stderr_b64":{"type":"string","description":"Raw stderr bytes, base64; at most limit decoded bytes."},"stdout_offset":{"type":"integer","description":"Next stdout byte offset: supplied offset plus decoded bytes returned.","minimum":0,"maximum":9007199254740991},"stderr_offset":{"type":"integer","description":"Next stderr byte offset, independent of stdout and all other readers.","minimum":0,"maximum":9007199254740991},"stdout_more":{"type":"boolean","description":"The read reached its limit without observing EOF. Another read may still be empty."},"stderr_more":{"type":"boolean","description":"The same for stderr. False is a current EOF observation, not a promise of no future output."},"diagnostic_b64":{"type":"string","description":"Separate daemon-captured wrapper diagnostic, base64, at most 65536 decoded bytes. Repeated in full on every read; not part of either guest-file offset. Legacy reads cannot consume it."},"diagnostic_truncated":{"type":"boolean","description":"The wrapper diagnostic exceeded 65536 bytes or guest-agent capture was truncated; the diagnostic is incomplete."}},"required":["execution_id","stdout_b64","stderr_b64","stdout_offset","stderr_offset","stdout_more","stderr_more","diagnostic_b64","diagnostic_truncated"]},"CreditLot":{"type":"object","properties":{"id":{"type":"string","description":"Immutable lot identifier."},"kind":{"type":"string","enum":["plan_grant","admin_grant","topup","adjust"]},"amount_micros":{"type":"integer","description":"Original credit amount; one USD is 1,000,000 micros."},"remaining_micros":{"type":"integer","description":"Unspent amount, before account debt and active reservations."},"expires_at":{"type":"string","description":"Exclusive expiry instant, RFC 3339 UTC."},"created_at":{"type":"string","description":"Issue time, RFC 3339 UTC."}},"required":["id","kind","amount_micros","remaining_micros","expires_at","created_at"],"additionalProperties":false},"CreditGrant":{"type":"object","properties":{"period_start":{"type":"string","description":"Confirmed plan period start, RFC 3339 UTC; calendar month for operator plans."},"period_end":{"type":"string","description":"Exclusive end of the current grant period, RFC 3339 UTC."},"granted_micros":{"type":"integer","description":"Actually issued this period, including upgrade differences. Downgrading does not lower this high-water mark."},"used_micros":{"type":"integer","description":"Issued minus remaining. Includes spending, debt settlement, and removed credits; not solely model usage."}},"required":["period_start","period_end","granted_micros","used_micros"],"additionalProperties":false},"CreditEntry":{"type":"object","properties":{"id":{"type":"integer","description":"Journal sequence number. Entries are returned newest first."},"lot_id":{"type":["string","null"],"description":"The lot a credit, expire or debt_settle entry applies to. Always null on debit and clawback entries."},"kind":{"type":"string","enum":["credit","debit","expire","clawback","debt_settle"]},"amount_micros":{"type":"integer","description":"Nonnegative amount. credit adds a lot; debit and clawback consume credits and may create debt; expire removes a remainder; debt_settle applies a new credit to existing debt. Do not sum kinds without their direction."},"debt_micros":{"type":"integer","description":"New debt created by this entry, not the running account debt balance."},"model":{"type":["string","null"],"description":"Billed model for a debit, otherwise null."},"run_id":{"type":["string","null"],"description":"Funded run identifier for a debit, otherwise null."},"step":{"type":["integer","null"],"description":"Model-call sequence within the run, otherwise null."},"created_at":{"type":"string","description":"Entry time, RFC 3339 UTC."}},"required":["id","lot_id","kind","amount_micros","debt_micros","model","run_id","step","created_at"],"additionalProperties":false},"Credits":{"type":"object","properties":{"balance_micros":{"type":"integer","description":"Sum of unexpired, unspent lot remainders. One USD is 1,000,000 micros."},"available_micros":{"type":"integer","description":"Balance minus debt and held credits, saturated at the negative safe-integer bound. May be negative; zero while frozen. This is not rollout admission."},"held_micros":{"type":"integer","description":"Credits reserved for active model calls."},"debt_micros":{"type":"integer","description":"Outstanding debt, settled by future credit issuance."},"frozen":{"type":"boolean","description":"No recognized plan or account suspended. Credits are retained, but expiry continues."},"grant":{"oneOf":[{"$ref":"#/components/schemas/CreditGrant"},{"type":"null"}]},"lots":{"type":"array","items":{"$ref":"#/components/schemas/CreditLot"},"description":"Only unexpired lots with a positive remainder, earliest expiry first. Exhausted and expired lots are represented in journal history."},"entries":{"type":"array","items":{"$ref":"#/components/schemas/CreditEntry"}},"next_before":{"type":["integer","null"],"description":"Pass as before for older entries; null on the final page. Balances and lots are current on every page."}},"required":["balance_micros","available_micros","held_micros","debt_micros","frozen","grant","lots","entries","next_before"],"additionalProperties":false},"Usage":{"type":"object","properties":{"period":{"$ref":"#/components/schemas/UsagePeriod","description":"The period this account is billed on. NOT necessarily the window that was measured — see `from` and `to`, which are."},"from":{"type":"string","description":"The start of the window these figures cover, RFC 3339."},"to":{"type":"string","description":"The end of it, RFC 3339, and worth reading rather than assuming. A `to` in the future is answered as now, because the future holds no usage; everything else is the instant you asked for."},"usage":{"$ref":"#/components/schemas/UsageTotals"},"degraded":{"type":"boolean","description":"A hypervisor could not be reached, so every figure above may be too small. This is NOT a 503, and it is not a gap: each figure is a sum across the fleet, so a host that did not answer leaves a total that is quietly short rather than an obviously missing row. Do not reconcile against an invoice while this is true — retry, and it clears when the host comes back."},"unmetered":{"type":"boolean","description":"The same shortfall from the other cause, and it is separate because it does NOT clear on its own: a hypervisor is up and running a daemon older than the meter, so it has no hours to report. Waiting does not fix this one."},"reported_through":{"type":["string","null"],"description":"The last UTC day (`YYYY-MM-DD`) whose usage has settled for billing — a contiguous prefix, so a day still being held back stops the count where it is. Null when none of the window has settled yet. This is not a caveat on the totals: those are live from the ledger and true through `to`. It answers the other question, which is how much of the same window has reached the billing system, and it is the one to check before comparing these numbers with an invoice. NULL, not absent, until something has settled — which is every account today."}},"required":["period","from","to","usage","degraded","unmetered","reported_through"]},"UsagePeriod":{"type":"object","properties":{"start":{"type":"string","description":"RFC 3339."},"end":{"type":"string","description":"RFC 3339."},"source":{"type":"string","description":"`subscription` when the boundary came from your plan’s own billing period, which is what an invoice is anchored to — usually the renewal date, and the end of the trial while you are in one. `subscription-projected` in the short gap between a renewal happening and our hearing about it: `start` is the previous period’s end and is exact, and `end` is that plus the previous period’s length, which is a duration rather than a billing rule — so it can be a few days out across a short month, and a whole term out if a plan change took effect at that same renewal. Treat a projected `end` as provisional and never quote it as a renewal date; `start` is exact and safe to use. `calendar-month` when there is no billing period to take it from, in which case the period is the current UTC month. Do NOT read `calendar-month` as “no subscription”: it also covers a subscription whose period start we do not hold, which is possible for a plan billed over anything longer than a month. This API does not distinguish the two, so do not branch on it to decide whether an account has a plan — read `from` and `to` for what was measured. New values may be added, so treat this as an open set."}},"required":["start","end","source"]},"UsageTotals":{"type":"object","properties":{"run_hours":{"type":"number","description":"Wall-clock hours computers on this account spent running."},"vcpu_hours":{"type":"number","description":"Those hours weighted by cores."},"ram_gb_hours":{"type":"number","description":"And by memory."},"snapshot_gb_hours":{"type":"number","description":"The integral of snapshot bytes held, in GB-hours."},"snapshot_gb_months":{"type":"number","description":"The same integral in the unit snapshots are priced in."},"disk_gb_hours":{"type":"number","description":"The integral of provisioned computer disk, running or not — a stopped computer still holds its disk. Kept apart from the snapshot figures on purpose: disks are provisioned at create and released at delete, snapshots come and go under your plan’s retention and your own deletions, and the two have different remedies."},"disk_gb_months":{"type":"number","description":"The same integral in the unit disk is priced in."},"computers":{"type":"array","items":{"$ref":"#/components/schemas/UsageComputer"},"description":"The per-computer breakdown, which is what makes a total checkable. ABSENT when the API key is scoped to a workspace: usage is metered per account, so these lines cover the whole account and would name computers outside the key’s scope. The totals are still account-wide either way."}},"required":["run_hours","vcpu_hours","ram_gb_hours","snapshot_gb_hours","snapshot_gb_months","disk_gb_hours","disk_gb_months"]},"UsageComputer":{"type":"object","properties":{"id":{"type":"string","description":"The computer this line is for. The time one of your own templates spends booting its seed for fast launches is billed under the seed’s own id and name, which `GET /computers` never lists; that line reads `gone` once the seed is deleted."},"name":{"type":"string","description":"What it was called. The name from a host that still holds it wins."},"run_hours":{"type":"number"},"vcpu_hours":{"type":"number"},"ram_gb_hours":{"type":"number"},"gone":{"type":"boolean","description":"Present and `true` when this computer is no longer on the fleet. It ran during the window and was deleted, which is why it is billed for and not in `GET /computers`."}},"required":["id","run_hours","vcpu_hours","ram_gb_hours"]},"Secret":{"type":"object","properties":{"id":{"type":"string","description":"`csec-` and sixteen hex characters. What a binding names as `secret_id`."},"name":{"type":"string","description":"Its name, as sent with leading and trailing spaces trimmed. Up to 60 characters. Unique within its scope, ignoring ASCII case: names that differ only in ASCII case collide (`409`, `reason: \"name_taken\"`)."},"workspace_id":{"type":["string","null"],"description":"The workspace it belongs to, or `null` for one available account-wide."},"revision_id":{"type":"string","description":"`csr-` and twenty-four hex characters. Moves every time the value is replaced; a replace or delete sends back the one it read."},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"updated_at":{"type":"string","description":"RFC 3339 timestamp of the last replace, or of the create."},"last_used_at":{"type":["string","null"],"description":"When it was last delivered to a computer. `null` until it has been."}},"required":["id","name","workspace_id","revision_id","created_at","updated_at","last_used_at"]},"SecretList":{"type":"object","properties":{"secrets":{"type":"array","items":{"$ref":"#/components/schemas/Secret"}},"delivery":{"type":"boolean","description":"Whether a secret can be bound to a computer and delivered into it on this platform. While `false`, secrets can be stored but a binding is refused."},"limits":{"type":"object","properties":{"name_max_chars":{"type":"integer","description":"The longest name, in characters."},"value_max_bytes":{"type":"integer","description":"The largest value, in bytes of UTF-8."},"active_per_account":{"type":"integer","description":"How many secrets the account may hold at once, across every workspace. Deleting one frees a place; a workspace holding active secrets cannot be deleted. Secrets orphaned by older workspace deletions remain unreachable and still count; this safeguard does not repair them."},"created_per_account":{"type":"integer","description":"How many secrets the account may create over its lifetime, deleted ones included. Deleting does not free a place; support can raise it."}},"required":["name_max_chars","value_max_bytes","active_per_account","created_per_account"]}},"required":["secrets","delivery","limits"]},"ApiKey":{"type":"object","properties":{"id":{"type":"string","description":"`key-` and twelve hex characters. What an audit entry names as `apikey:<id>`."},"name":{"type":["string","null"],"description":"Its label, up to 60 characters, or `null` when it was minted without one. For a hosted-MCP (OAuth) credential, which only `GET /whoami` answers, `Connected app: ` followed by the connected app’s name, up to 100 characters in all."},"prefix":{"type":"string","description":"The first twelve characters of the key and an ellipsis, for recognising it. Not a secret. `oauth` for a hosted-MCP (OAuth) credential, as `GET /whoami` answers it."},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"last_used_at":{"type":["string","null"],"description":"RFC 3339 timestamp of its last authenticated request, or `null` until it has one."},"workspace_id":{"type":["string","null"],"description":"The workspace it is confined to, or `null` for the whole account."},"workspace_name":{"type":["string","null"],"description":"That workspace’s name, or `null` for an account-wide key."},"manage_keys":{"type":"boolean","description":"Whether this key may use `/api-keys`. With it a key lists its holder’s keys; creating and revoking also need the holder to be a member or owner, so a viewer’s key can only list. Set only from the dashboard; no key can set it."},"minted_by_key_id":{"type":["string","null"],"description":"The id of the API key that minted this one over the API, or `null` for a key minted from the dashboard. Kept after that key is revoked."}},"required":["id","name","prefix","created_at","last_used_at","workspace_id","workspace_name","manage_keys","minted_by_key_id"]},"ApiKeyCreated":{"type":"object","properties":{"id":{"type":"string","description":"As on `ApiKey`."},"name":{"type":["string","null"],"description":"As on `ApiKey`."},"prefix":{"type":"string","description":"As on `ApiKey`."},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"last_used_at":{"type":["string","null"],"description":"Always `null`: the key has not been used yet."},"workspace_id":{"type":["string","null"],"description":"As on `ApiKey`."},"workspace_name":{"type":["string","null"],"description":"As on `ApiKey`."},"manage_keys":{"type":"boolean","description":"As on `ApiKey`. Always `false` for a key minted over the API."},"minted_by_key_id":{"type":["string","null"],"description":"As on `ApiKey`. Always the id of the key that made this request."},"raw":{"type":"string","description":"The key: `com_` and 48 hex characters. **Shown here and never again** — store it now. Send it as `Authorization: Bearer <raw>`."}},"required":["id","name","prefix","created_at","last_used_at","workspace_id","workspace_name","manage_keys","minted_by_key_id","raw"]},"Whoami":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string","description":"`usr-` and sixteen hex characters."},"email":{"type":["string","null"],"description":"The address its holder signs in with. `null` for a workspace-scoped key, which may be in an end customer’s hands."},"name":{"type":["string","null"],"description":"Their display name, or `null` when they have not set one. Always `null` for a workspace-scoped key, for the same reason as `email`."}},"required":["id","email","name"]},"account":{"type":"object","properties":{"id":{"type":"string","description":"`acc-` and sixteen hex characters."},"name":{"type":["string","null"],"description":"The account’s name, or `null` when it has none. Always `null` for a workspace-scoped key: signup names an account after its owner, so it is usually a person’s name."},"plan":{"type":["string","null"],"description":"The id of the plan the account is on, such as `solo`, `studio` or `fleet` (the plan sold as Swarm), without its display name. `null` for a workspace-scoped key. `GET /account` gives the plan with its display name in `plan.label`, and its ceilings."},"status":{"type":"string","description":"`active`, or `suspended` when the account may not use resources."}},"required":["id","name","plan","status"]},"role":{"type":"string","description":"`owner`, `member` or `viewer`: the holder’s role on the account now, which is what the key acts with."},"workspace":{"oneOf":[{"$ref":"#/components/schemas/Workspace"},{"type":"null"}]},"key":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"null"}]}},"required":["user","account","role","workspace","key"]},"Workspace":{"type":"object","properties":{"id":{"type":"string","description":"`wsp-` and twelve hex characters. What a computer’s `workspace_id` names."},"name":{"type":"string","description":"Its name, unique within the account. Up to 40 characters."},"created_at":{"type":"string","description":"RFC 3339 timestamp."}},"required":["id","name","created_at"]},"WorkspaceDeleted":{"type":"object","properties":{"ok":{"type":"boolean","description":"Always `true`. A failure is a status code, not `ok: false`."},"revoked_keys":{"type":"integer","description":"How many API keys were confined to the workspace and were revoked with it, whoever held them."}},"required":["ok","revoked_keys"]},"WorkspaceMember":{"type":"object","properties":{"user_id":{"type":"string","description":"`usr-` and sixteen hex characters."},"email":{"type":"string","description":"The address they sign in with."},"name":{"type":["string","null"],"description":"Their display name, or `null` when they have not set one."},"role":{"type":"string","description":"`owner`, `member` or `viewer` — their role on the account, which is their role in every workspace."},"accepted_at":{"type":"string","description":"RFC 3339 timestamp of when they joined the account."},"suspended":{"type":"boolean","description":"`true` for global sign-in suspension. They are still on the account. This is independent of `seat_suspended`; either or both flags can be `true`."},"seat_suspended":{"type":"boolean","description":"`true` when this member is waiting for a seat on this account and cannot act on it. This is independent of sign-in suspension (`suspended`) and returns to `false` when their seat is restored."}},"required":["user_id","email","name","role","accepted_at","suspended","seat_suspended"]},"PublicUrl":{"type":"object","properties":{"id":{"type":"string","description":"`pu-` and sixteen hex characters."},"url":{"type":"string","description":"The address to open: `https://<label>.<base_domain>`, on port 443, with no port in it."},"hostname":{"type":"string","description":"`<label>.<base_domain>`."},"label":{"type":"string","description":"The name before the domain: 3 to 40 lowercase letters, digits and hyphens, a letter or digit at each end.","minLength":3,"maxLength":40,"pattern":"^[a-z0-9][a-z0-9-]*[a-z0-9]$"},"port":{"type":"integer","description":"The port inside the computer this URL serves. The app must listen on `0.0.0.0`, not `127.0.0.1`.","minimum":1,"maximum":65535},"access":{"type":"string","enum":["public","private"],"description":"`public`: anyone with the link. `private`: only a request carrying an API key of this account as `Authorization: Bearer` (a key confined to a workspace, only for that workspace’s computers), or a browser signed in to this URL through the dashboard’s Open; neither credential reaches the app. See `POST /computers/{id}/public-urls`."},"disabled":{"type":"boolean","description":"`true` when the platform has turned this URL off, by its name, its computer or its account; it answers 404 until the platform turns it back on. Deleting it and making it again does not."},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"updated_at":{"type":"string","description":"RFC 3339 timestamp of the last change to its port or access."}},"required":["id","url","hostname","label","port","access","disabled","created_at","updated_at"]},"PublicUrlList":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether this computer’s URLs can answer here: `false` where the feature is off or the account’s plan has no public URLs, with `reason` saying which."},"reason":{"type":"string","description":"Why `enabled` is false. Absent when it is true."},"base_domain":{"type":"string","description":"The domain every label sits under, e.g. `mandala.dev`."},"limit":{"type":"integer","description":"How many public URLs this computer may have on the account’s plan."},"public_urls":{"type":"array","items":{"$ref":"#/components/schemas/PublicUrl"}}},"required":["enabled","base_domain","limit","public_urls"]},"AgentRun":{"type":"object","properties":{"id":{"type":"string","description":"`run_` and twenty-four hex characters."},"computer_id":{"type":"string"},"thread_id":{"type":["string","null"],"description":"The thread the run is kept in, once it is: `null` until a new thread has something in it (a run that has not acted yet, or failed before it did), and when the thread was deleted. Continue it with `thread_id` on the next run."},"status":{"type":"string","enum":["running","completed","failed","cancelled","interrupted"],"description":"`completed`: the loop ended on its own, and `result.stop` says how — only `end_turn` means the task was finished. `failed`: see `error`. `cancelled`: stopped by a cancel. `interrupted`: the control plane restarted while it was going (`error.status` 503), or the run stopped without its ending being recorded (`error.status` 500); it was not resumed."},"prompt":{"type":"string","description":"The prompt, at most 32,000 characters of it."},"model":{"type":"string","description":"The model the run was started with."},"max_steps":{"type":"integer"},"steps":{"type":"integer","description":"How many actions it has taken so far."},"cost_usd":{"type":"number","description":"Funded runs: the charge so far, in USD."},"result":{"$ref":"#/components/schemas/AgentResult"},"error":{"type":"object","properties":{"error":{"type":"string","description":"What went wrong, in a sentence."},"status":{"type":"integer","description":"The status the agent route would have answered it with."},"reason":{"type":"string","description":"The one-word classification, where there is one — see `Error`."},"top_up_url":{"type":"string","description":"Where to add credits, when the run failed for want of them."}},"required":["error","status"]},"steps_taken":{"type":"array","items":{"$ref":"#/components/schemas/AgentStep"},"description":"Every action so far, in order. Empty in a list."},"created_at":{"type":"string"},"updated_at":{"type":"string","description":"When it last took a step or changed status."},"ended_at":{"type":["string","null"],"description":"When it ended; `null` while running."}},"required":["id","computer_id","thread_id","status","prompt","model","max_steps","steps","steps_taken","created_at","updated_at","ended_at"]},"AgentRunList":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/AgentRun"}},"has_more":{"type":"boolean","description":"Whether another page follows; pass the last run’s id as `before` to get it."}},"required":["runs","has_more"]},"AgentThreadSummary":{"type":"object","properties":{"id":{"type":"string","description":"`thr_` and twenty-four hex characters."},"computer_id":{"type":"string","description":"The computer its runs drove. A thread is continued only there."},"title":{"type":"string","description":"The first line of the prompt of the first run kept in it (not of any `messages` it was started with), at most 80 characters."},"turns":{"type":"integer","description":"How many turns it holds: a prompt and an answer per run, plus any history it was started with."},"created_at":{"type":"string"},"updated_at":{"type":"string","description":"When a run last used it: kept an exchange in it, or began continuing it."}},"required":["id","computer_id","title","turns","created_at","updated_at"]},"AgentThreadList":{"type":"object","properties":{"threads":{"type":"array","items":{"$ref":"#/components/schemas/AgentThreadSummary"}},"has_more":{"type":"boolean","description":"Whether another page follows; pass the last thread’s id as `before` to get it."}},"required":["threads","has_more"]},"AgentThreadTurn":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant"]},"text":{"type":"string","description":"The prompt, or the run’s closing text. At most 32,000 characters are kept."},"stop":{"type":"string","description":"Assistant turns: how the run ended, as `AgentResult.stop`, or `error` or `aborted`; `running` while its run is going, and `interrupted` when the control plane restarted before it finished. `running`, `aborted` and `interrupted` are runs that may have done more than they reported."},"error":{"type":"string","description":"Assistant turns of a failed run: why it failed."},"steps":{"type":"integer","description":"Assistant turns: how many actions the run took."},"cost_usd":{"type":"number","description":"Assistant turns of a funded run: what it cost."},"created_at":{"type":"string"}},"required":["role","text","created_at"]},"AgentThread":{"type":"object","properties":{"id":{"type":"string","description":"`thr_` and twenty-four hex characters."},"computer_id":{"type":"string"},"title":{"type":"string"},"turns":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/AgentThreadTurn"},"description":"Every turn, oldest first."}},"required":["id","computer_id","title","turns","created_at","updated_at","messages"]},"Webhook":{"type":"object","properties":{"id":{"type":"string","description":"`whk-` and sixteen hex characters."},"url":{"type":"string","description":"Where deliveries are POSTed. `https://` only. The URL as the platform parsed it, which can differ in spelling from what you sent: the host lower-cased, a default port dropped, a bare origin ending in `/`."},"description":{"type":"string","description":"Free text, for your listing. Empty when you gave none."},"events":{"type":"array","items":{"type":"string"},"description":"The event types this subscription receives. **Empty means every type.** The vocabulary is the event socket’s (the computer’s `events_url`) less `file.changed`: `window.opened`, `window.closed`, `window.focused`, `window.blurred`, `clipboard.changed`, `process.exited`, `computer.idle`, `computer.ready`, `computer.started`, `computer.stopped`, `computer.suspended`."},"computers":{"type":"array","items":{"type":"string"},"description":"The computer ids this subscription receives events for. **Empty means every computer in scope.** Not checked against your computers when you set it — a subscription may name a computer you are about to create."},"enabled":{"type":"boolean","description":"Whether deliveries are made. Set `false` by the platform when an endpoint has failed for a day, or when the account moves to a plan without webhooks — see `disabled_reason`. Back to `true` by you with `PATCH`, which starts fresh, or, for `plan`, by the platform when you choose a plan again, which starts fresh too."},"disabled_reason":{"type":["string","null"],"description":"Why `enabled` is false: `customer` when you disabled it, `failing` when the platform did — a delivery ran out of attempts and nothing had been accepted for 24 hours, counted from the later of the last 2xx and when the subscription was last enabled — and `plan` when the account moved to a plan without webhooks. A `plan` subscription is enabled again, oldest first, as many as the plan allows, when the account chooses a plan that includes webhooks. `null` while enabled.","enum":["customer","failing","plan",null]},"disabled_at":{"type":["string","null"],"description":"RFC 3339 timestamp, or `null` while enabled."},"last_success_at":{"type":["string","null"],"description":"When the endpoint last answered 2xx to any delivery. `null` until it has."},"last_failure_at":{"type":["string","null"],"description":"When a delivery attempt last failed. `null` until one has."},"last_status":{"type":["integer","null"],"description":"The HTTP status of the newest attempt, whatever it was. `null` before any attempt, or when the newest got no answer."},"workspace_id":{"type":"string","description":"The workspace this subscription is confined to. Present only when it was created with a workspace-scoped API key; absent on an account-wide subscription."},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"updated_at":{"type":"string","description":"RFC 3339 timestamp."}},"required":["id","url","description","events","computers","enabled","disabled_reason","disabled_at","last_success_at","last_failure_at","last_status","created_at","updated_at"]},"WebhookCreated":{"type":"object","properties":{"id":{"type":"string","description":"`whk-` and sixteen hex characters."},"url":{"type":"string","description":"Where deliveries are POSTed."},"description":{"type":"string"},"events":{"type":"array","items":{"type":"string"},"description":"As on `Webhook`."},"computers":{"type":"array","items":{"type":"string"},"description":"As on `Webhook`."},"enabled":{"type":"boolean"},"disabled_reason":{"type":["string","null"]},"disabled_at":{"type":["string","null"]},"last_success_at":{"type":["string","null"]},"last_failure_at":{"type":["string","null"]},"last_status":{"type":["integer","null"]},"workspace_id":{"type":"string","description":"As on `Webhook`: present only on a workspace-confined subscription."},"created_at":{"type":"string","description":"RFC 3339 timestamp."},"updated_at":{"type":"string","description":"RFC 3339 timestamp."},"secret":{"type":"string","description":"The signing secret: `whsec_` and 44 characters of base64. **Shown here and never again** — store it now. Every delivery is signed with it; see the resource description for how to verify."}},"required":["id","url","description","events","computers","enabled","disabled_reason","disabled_at","last_success_at","last_failure_at","last_status","created_at","updated_at","secret"]},"WebhookEvent":{"oneOf":[{"$ref":"#/components/schemas/WebhookComputerEvent"},{"$ref":"#/components/schemas/WebhookGapEvent"},{"$ref":"#/components/schemas/WebhookTestEvent"}],"description":"What a delivery POSTs: an event exactly as the computer’s `events_url` socket frames it, a `gap` frame, or a `webhook.test` from `POST /webhooks/{id}/test`. Switch on `type`, and ignore one you do not know."},"WebhookComputerEvent":{"type":"object","properties":{"seq":{"type":"integer","description":"Orders events on one computer and nothing else. It restarts when the host’s daemon does."},"cursor":{"type":"string","description":"Opaque. Add it as `since=` to the computer’s `events_url` to read on from this event."},"at":{"type":"string","description":"When the host recorded it, RFC 3339 in UTC."},"type":{"type":"string","description":"One of the subscription vocabulary: `window.opened`, `window.closed`, `window.focused`, `window.blurred`, `clipboard.changed`, `process.exited`, `computer.idle`, `computer.ready`, `computer.started`, `computer.stopped`, `computer.suspended`. Treat a type you do not know as one to ignore: the vocabulary can grow."},"computer":{"type":"string","description":"The computer’s id."},"source":{"type":"string","description":"`daemon` when the host saw it, `guest` when the guest said so.","enum":["daemon","guest"]},"data":{"type":"object","description":"The payload, shaped by `type`. Absent when the event has none."}},"required":["seq","cursor","at","type","computer","source"]},"WebhookGapEvent":{"type":"object","properties":{"cursor":{"type":"string","description":"The computer’s own position after the gap: read on from here with `since=`."},"at":{"type":"string","description":"RFC 3339 in UTC."},"type":{"type":"string","const":"gap"},"computer":{"type":"string","description":"The computer’s id."},"source":{"type":"string","const":"daemon"},"data":{"type":"object","properties":{"detail":{"type":"string","description":"That events happened this computer can no longer replay."},"oldest_cursor":{"type":"string","description":"The oldest position it still holds, when it holds any."}},"required":["detail"]}},"required":["cursor","at","type","computer","source","data"]},"WebhookTestEvent":{"type":"object","properties":{"at":{"type":"string","description":"When the test was asked for, RFC 3339 in UTC."},"type":{"type":"string","const":"webhook.test"},"computer":{"type":"string","description":"Empty: a test is about no computer.","const":""},"source":{"type":"string","const":"control-plane"},"data":{"type":"object","properties":{"subscription":{"type":"string","description":"The subscription tested."}},"required":["subscription"]}},"required":["at","type","computer","source","data"]},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string","description":"`whd-` and sixteen hex characters: the `webhook-id` header this delivery carried, fixed across attempts."},"event_type":{"type":"string","description":"The event’s `type`. `gap` for a gap frame; `webhook.test` for a test delivery."},"computer":{"type":"string","description":"The computer the event is about. Empty on a test delivery."},"cursor":{"type":"string","description":"The event’s own `cursor` — what to add as `since=` to the computer’s `events_url` to read on from it. Two kinds of delivery carry something else, which is not a socket position and must not be sent as `since=`: a test delivery’s is `test:` and its delivery id, and an event that arrived without a cursor of its own is keyed `body:` and a hash of its bytes."},"state":{"type":"string","description":"`pending` (an attempt is scheduled), `in_flight` (one is running), `delivered` (a 2xx came back), `exhausted` (eight attempts failed) or `dropped` (the subscription was disabled or deleted first).","enum":["pending","in_flight","delivered","exhausted","dropped"]},"attempts":{"type":"integer","description":"How many times it has been sent. Eight is the last."},"next_at":{"type":["string","null"],"description":"When the next attempt is due, RFC 3339. `null` once the delivery is finished."},"attempted_at":{"type":["string","null"],"description":"When the newest attempt started. `null` before the first."},"last_status":{"type":["integer","null"],"description":"The HTTP status of the newest attempt, or `null` when it got no answer."},"last_error":{"type":["string","null"],"description":"One line about the newest failure, for example `timeout`, `dns`, `refused`, `reset`, `unreachable`, `tls`, `redirect`, `address refused` or `status NNN`; other values are possible. A `dropped` delivery says why it was dropped. `null` after a success and before any attempt."},"delivered_at":{"type":["string","null"],"description":"When the 2xx came back. `null` otherwise."},"created_at":{"type":"string","description":"RFC 3339 timestamp: when the event reached the queue."}},"required":["id","event_type","computer","cursor","state","attempts","next_at","attempted_at","last_status","last_error","delivered_at","created_at"]}}}}