{"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 or through explicit browser approval 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_...\" {ORIGIN}/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\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\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.\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."},{"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."}],"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\nRequires the `viewer` role or higher.","operationId":"getAccount","tags":["Account"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"account\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"account\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}},"/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. `name` is what you pass as `template` to create.\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, 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"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.templates.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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\nTHE NAMESPACE IS YOUR ACCOUNT. `metadata.namespace` has to be your account id, which is the `namespace` 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\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.\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 BUILD STEPS 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 no build steps 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 BUILD STEPS 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 no build steps 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"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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. Point an editor at this URL to get 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. 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\nThe `$id` in the response is this URL, so a `$ref` to it resolves back here.\n\nRequires the `viewer` role or higher.","operationId":"getTemplateSchema","tags":["Templates"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.templates.schema()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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, and comparing it tells you whether an edit means a rebuild. A document that names a parent in `spec.from` has no `build_digest`, because that cannot be computed without the parent’s.\n\nRequires the `member` role or higher.","operationId":"validateTemplate","tags":["Templates"],"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 it can supply. Run `gorillad -build-template <file> -dry-run` there to see this document's build digest"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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 build steps 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\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"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    namespace = \"system\"\n    name = \"base\"\n    result = client.templates.get(namespace, name)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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.\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"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/builds":{"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\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 build steps 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\nOne build runs per hypervisor at a time. A `409` means a host is busy 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 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 `409` COSTS NO BUILD, and it does count as a request: the host was busy and started nothing, but it was asked. Everything else counts against the build allowance too, 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"],"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."}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"501":{"description":"No hypervisor in the fleet can build a template. Unlike a `409`, this does not clear on its own — retrying will not help until the platform can build, 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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"get":{"summary":"List builds","description":"Every build this account has started that the fleet still holds a record of, newest first.\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"],"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 response header rather than the rows."}],"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-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.builds.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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. Launching by that ref works today only where the family already exists on the fleet — see `POST /templates` for what a custom family can and cannot do yet. `error` says why a failed one failed, which for a `run:` step is the end of that step’s own output.\n\nRequires the `viewer` role or higher.","operationId":"getBuild","tags":["Builds"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.builds.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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\nRequires the `viewer` role or higher.","operationId":"getBuildProgress","tags":["Builds"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.builds.progress(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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.\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` means the stream itself could not go on and says nothing about the build; poll for the outcome.\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\nRequires the `viewer` role or higher.","operationId":"streamBuildProgress","tags":["Builds"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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: {\"error\":\"Stream interrupted\",\"request_id\":\"req_0123456789abcdef0123456789abcdef\"}\n\n"}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.\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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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);"}]}},"/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"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.sizes.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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. No `vnc` on these rows; fetch one computer to get its desktop credentials.\n\nRequires the `viewer` role or higher.","operationId":"listComputers","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."}],"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-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.computers.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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.\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 build is slower and comes back with `status: \"building\"`; poll the computer until it is `running`.\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.\n\nThe workspace comes from the API key, not from the body — a key scoped to a workspace creates in it, and there is no field to override that with.\n\nRequires the `member` role or higher.","operationId":"createComputer","tags":["Computers"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Defaults to a generated one."},"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."},"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 the account’s default template.\n\nThe two differ in what happens when the name is wrong. A short name this host does not have falls back to the default template, which is long-standing behaviour and stays; a ref that names nothing is refused with a 400 listing the refs that do exist. 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."},"ram_mb":{"type":"integer","description":"Defaults to the template’s."},"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."},"start":{"type":"boolean","description":"Start it once built. Defaults to true."},"resolution":{"type":"string","description":"`WIDTHxHEIGHTxDEPTH`, e.g. `1920x1080x24`. Defaults to `1280x800x24`. Create-time only — there is no route that changes it later."}}}}}},"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}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    result = client.computers.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"patch":{"summary":"Update a computer","description":"Rename it, resize it, or change how long it may idle before its host suspends it.\n\nSEND ONE OF THOSE THREE AT A TIME. `name`, the sizing group (`cpu`/`ram_mb`/`disk_gb`), and `idle_suspend_min` each refuse to travel with either of the others, and a request carrying two is a 400. They are separated because a resize needs the computer stopped and the other two do not, so one request could not honour both without silently applying half of it.\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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"On its own — not alongside a resize or `idle_suspend_min`."},"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. Zero disables idle suspend and pressure eviction, subject to the account’s never-suspend limit (Solo 0, Studio 1, Fleet 4); adding a pin beyond it returns 402. Existing pins are preserved."}}}}}},"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,"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    name = \"base\"\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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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. Its snapshots are KEPT by default and become orphans you can still clone from — pass `snapshots=delete` to purge them with it.\n\nA purge should be bound to what you were shown. Read `GET /computers/{id}/snapshots` first, then pass its `fingerprint` back as `expect`: the daemon refuses the sweep if the set has changed since. Without `expect` the purge is unguarded.\n\nRequires the `member` role or higher.","operationId":"deleteComputer","tags":["Computers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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`."}],"responses":{"200":{"description":"Deleted, and how many snapshots went with it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResult"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true,"snapshots_deleted":0}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.delete()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/computers/{id}/start":{"post":{"summary":"Start a computer","description":"Boots a stopped computer, or resumes a suspended one.\n\nRequires the `member` role or higher.","operationId":"startComputer","tags":["Computers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"resume_only","in":"query","required":false,"schema":{"type":"string","enum":["true"]},"description":"Resume only if a saved session still exists. Succeeds without booting a stopped computer."}],"responses":{"200":{"description":"Started, or no saved session remains when resume_only=true.","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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.start()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/computers/{id}/stop":{"post":{"summary":"Stop a computer","description":"Powers it off. The disk is kept; you stop paying for the memory.\n\nThe guest is asked to shut down and given time to do it. `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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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."}],"responses":{"200":{"description":"Stopped.","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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.stop()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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 — with a clock that is stale by however long it was suspended, until NTP catches up.\n\nRequires the `member` role or higher.","operationId":"suspendComputer","tags":["Computers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"responses":{"200":{"description":"Suspended.","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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.suspend()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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.\n\nRequires the `member` role or higher.","operationId":"restartComputer","tags":["Computers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"responses":{"200":{"description":"Restarting.","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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.restart()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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.\n\nRequires the `member` role or higher.","operationId":"cloneComputer","tags":["Computers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Name for the copy. Defaults to a generated one."}}}}}},"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":"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-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}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    name = \"base\"\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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst name = \"base\";\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));"}]}},"/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.\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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"requestBody":{"required":false,"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. 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 is a 409 rather than a move nobody needed."},"disk_gb":{"type":"integer","description":"Grow only, as everywhere else. The growth happens on the far side, after the copy."}}}}}},"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"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.moves.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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, 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`. 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.\n\nAn explicit `fresh=1` (or `fresh=true`) request against a suspended computer answers 409; start it before requesting a live frame. A concurrent lifecycle operation can also answer 409; retry after that operation finishes. A stopped computer answers 400. Start the computer before driving its desktop.\n\nRequires the `viewer` role or higher.","operationId":"takeScreenshot","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"fresh","in":"query","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Skip the cache and capture now. Pass this whenever the screenshot is feeding a decision."},{"name":"w","in":"query","required":false,"schema":{"type":"integer"},"description":"Downscale to this width and answer JPEG instead of PNG. For thumbnails."}],"responses":{"200":{"description":"The current screen as PNG, or JPEG with `w`. Suspended sessions return a saved JPEG marked X-GC-Frame: suspended.","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-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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);"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.\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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"504":{"description":"The guest accepted a window action but did not report its result before the deadline. The action may already have happened, so this is an uncertain outcome rather than permission to repeat it.\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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"executions\" + \"/\" + quote(\"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\", safe=\"\") + \"/\" + \"retained-output\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"], \"Content-Type\": \"application/json\"}\nbody = json.loads(\"{}\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"executions\" + \"/\" + encodeSegment(\"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\") + \"/\" + \"retained-output\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey, \"Content-Type\": \"application/json\"};\nconst body = {};\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());"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":""}],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"results\" + \"/\" + quote(\"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\", safe=\"\")\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"results\" + \"/\" + encodeSegment(\"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\"));\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]},"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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":""}],"responses":{"204":{"description":"No response body; the result is logically unavailable.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"results\" + \"/\" + quote(\"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\", safe=\"\")\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"DELETE\")\nwith urlopen(request) as response:\n    print(response.status)  # 204 No Content: do not decode JSON."},{"lang":"TypeScript","label":"TypeScript — direct HTTP (no released wrapper)","source":"// Direct HTTP: this operation has no wrapper in mandala-computer@0.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"results\" + \"/\" + encodeSegment(\"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\"));\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"DELETE\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(response.status); // 204 No Content: do not decode JSON."}]}},"/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. Combined payload/count/capture quotas are shared with retained output. All routes require member and refuse suspended accounts. 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or endpoint; a confirmed missing guest file is no such file in the guest.\n\nThe source computer or requested execution association is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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\nSource is unavailable or transferred size/digest differs from the nomination.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.\n\nRate or shared retained quota/concurrency limit; store capacity includes Retry-After:60.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.\n\nCurrent scope or private store unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"504":{"description":"The guest accepted a window action but did not report its result before the deadline. The action may already have happened, so this is an uncertain outcome rather than permission to repeat it.\n\nCapture deadline or cancellation; no incomplete object is published.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"artifacts\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"], \"Content-Type\": \"application/json\"}\nbody = json.loads(\"{\\\"path\\\":\\\"/tmp/report.bin\\\",\\\"expected_size\\\":20,\\\"expected_sha256\\\":\\\"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde\\\"}\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"artifacts\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey, \"Content-Type\": \"application/json\"};\nconst body = {\"path\":\"/tmp/report.bin\",\"expected_size\":20,\"expected_sha256\":\"e953dd561c40b036816a668e57812df30c911543f3fb9127ac677a3183d31dde\"};\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());"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":""}],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"504":{"description":"The guest accepted a window action but did not report its result before the deadline. The action may already have happened, so this is an uncertain outcome rather than permission to repeat it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"artifacts\" + \"/\" + quote(\"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\", safe=\"\")\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"artifacts\" + \"/\" + encodeSegment(\"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\"));\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]},"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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":""}],"responses":{"204":{"description":"No response body; later artifact reads are unavailable.","headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"504":{"description":"The guest accepted a window action but did not report its result before the deadline. The action may already have happened, so this is an uncertain outcome rather than permission to repeat it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"artifacts\" + \"/\" + quote(\"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\", safe=\"\")\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"DELETE\")\nwith urlopen(request) as response:\n    print(response.status)  # 204 No Content: do not decode JSON."},{"lang":"TypeScript","label":"TypeScript — direct HTTP (no released wrapper)","source":"// Direct HTTP: this operation has no wrapper in mandala-computer@0.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"artifacts\" + \"/\" + encodeSegment(\"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\"));\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"DELETE\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(response.status); // 204 No Content: do not decode JSON."}]}},"/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. 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":""}],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"504":{"description":"The guest accepted a window action but did not report its result before the deadline. The action may already have happened, so this is an uncertain outcome rather than permission to repeat it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"artifacts\" + \"/\" + quote(\"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\", safe=\"\") + \"/\" + \"download\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nwith urlopen(Request(url.removesuffix(\"/download\"), headers=headers)) as response:\n    metadata = json.load(response)\nrequest = Request(url, headers=headers, method=\"GET\")\nwith urlopen(request) as response:\n    data = response.read()\n    if len(data) != metadata[\"size\"] or hashlib.sha256(data).hexdigest() != metadata[\"sha256\"]:\n        raise ValueError(\"Artifact integrity mismatch\")\n    Path(\"download.bin\").write_bytes(data)\n    print(dict(response.headers))"},{"lang":"TypeScript","label":"TypeScript — direct HTTP (no released wrapper)","source":"// Direct HTTP: this operation has no wrapper in mandala-computer@0.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"artifacts\" + \"/\" + encodeSegment(\"art_b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5\") + \"/\" + \"download\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst metadataResponse = await fetch(url.href.replace(/\\/download$/, \"\"), {headers});\nif (!metadataResponse.ok) throw new Error(await metadataResponse.text());\nconst metadata = await metadataResponse.json();\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconst bytes = new Uint8Array(await response.arrayBuffer());\nif (bytes.length !== metadata.size || createHash(\"sha256\").update(bytes).digest(\"hex\") !== metadata.sha256) throw new Error(\"Artifact integrity mismatch\");\nawait writeFile(\"download.bin\", bytes);\nconsole.log(Object.fromEntries(response.headers));"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"resultId","in":"path","required":true,"schema":{"type":"string"},"description":""},{"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-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"results\" + \"/\" + quote(\"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\", safe=\"\") + \"/\" + \"output\"\nurl += \"?\" + urlencode({\"stream\":\"stdout\",\"offset\":\"0\"})\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\nwith urlopen(request) as response:\n    data = response.read()\n    Path(\"download.bin\").write_bytes(data)\n    print(dict(response.headers))"},{"lang":"TypeScript","label":"TypeScript — direct HTTP (no released wrapper)","source":"// Direct HTTP: this operation has no wrapper in mandala-computer@0.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"results\" + \"/\" + encodeSegment(\"res_c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6\") + \"/\" + \"output\");\nurl.search = new URLSearchParams({\"stream\":\"stdout\",\"offset\":\"0\"}).toString();\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconst bytes = new Uint8Array(await response.arrayBuffer());\nawait writeFile(\"download.bin\", bytes);\nconsole.log(Object.fromEntries(response.headers));"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.\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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"501":{"description":"No hypervisor in the fleet can build a template. Unlike a `409`, this does not clear on its own — retrying will not help until the platform can build, 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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"signals\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"signals\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}},"/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. 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. 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.\n\nRequires the `member` role or higher.","operationId":"listActivities","tags":["Activities"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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 changes cursor. Invalid or expired change cursors return 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"activities\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"activities\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"activity","in":"path","required":true,"schema":{"type":"string"},"description":""}],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"activities\" + \"/\" + quote(\"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7\", safe=\"\")\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"activities\" + \"/\" + encodeSegment(\"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7\"));\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}},"/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 most8192 bytes; items at most896 bytes. An existing activity without versions returns an empty list; missing history is404; unavailable owner/history/current scope is503. 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. Browser controls and client helpers are separate successors.\n\nRequires the `member` role or higher.","operationId":"getActivityResults","tags":["Activities"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"activity","in":"path","required":true,"schema":{"type":"string"},"description":""}],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"activities\" + \"/\" + quote(\"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7\", safe=\"\") + \"/\" + \"results\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"activities\" + \"/\" + encodeSegment(\"act_d4e5f6a7d4e5f6a7d4e5f6a7d4e5f6a7\") + \"/\" + \"results\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}},"/computers/{id}/input":{"post":{"summary":"Send input","description":"Moves the pointer, clicks, drags, scrolls, types or presses keys.\n\nTwo request shapes are accepted. The flat one below is this API’s own. The other is a 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\nCoordinates are absolute, in the screen space `resolution` reports.\n\n`type` requires non-empty valid Unicode text, at most 400 characters. ASCII keeps its existing physical-key timing. Other characters use GTK Unicode composition 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\nRequires the `member` role or higher.","operationId":"sendInput","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"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."},"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 `hold_key`, the keys to hold."},"key":{"type":"string","description":"A chord such as `ctrl+c`, for `key`. For `paste`: ctrl+v (default) or ctrl+shift+v."},"keys":{"type":"array","items":{"type":"string"},"description":"The chord as separate keys."},"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."},"scroll_amount":{"type":"integer","description":"The computer-use spelling of `amount`."},"duration":{"type":"number","description":"Seconds, for `wait` and `hold_key`."}},"required":["action"]}}}},"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":{"$ref":"#/components/schemas/InputResult"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"ok":true}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/computers/{id}/exec":{"post":{"summary":"Run a command","description":"Runs a command inside the guest and waits for it, up to `timeout_s`.\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 suspended computer is resumed to run the command.\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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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`."},"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 nothing else 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."},"background":{"type":"boolean","description":"Answer with a handle instead of a result."},"retain_output":{"$ref":"#/components/schemas/RetainOutput"},"cwd":{"type":"string","description":"Working directory inside the guest."},"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."}},"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-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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\nOnly commands this API started can be read this way.\n\nRequires the `member` role or higher.","operationId":"readCommand","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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\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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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. Lost may be observed briefly before the handle is removed.\n\nRequires the `member` role or higher.","operationId":"getExecution","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"executions\" + \"/\" + quote(\"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\", safe=\"\")\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"executions\" + \"/\" + encodeSegment(\"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\"));\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}},"/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. Empty success means the existing files had no bytes at the supplied positions. This foundation 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"executions\" + \"/\" + quote(\"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\", safe=\"\") + \"/\" + \"output\"\nurl += \"?\" + urlencode({\"stdout_offset\":\"0\",\"stderr_offset\":\"0\"})\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\nwith urlopen(request) as response:\n    result = json.load(response)\n    print(result)\n    Path(\"stdout.bin\").write_bytes(base64.b64decode(result[\"stdout_b64\"]))\n    Path(\"stderr.bin\").write_bytes(base64.b64decode(result[\"stderr_b64\"]))"},{"lang":"TypeScript","label":"TypeScript — direct HTTP (no released wrapper)","source":"// Direct HTTP: this operation has no wrapper in mandala-computer@0.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"executions\" + \"/\" + encodeSegment(\"exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\") + \"/\" + \"output\");\nurl.search = new URLSearchParams({\"stdout_offset\":\"0\",\"stderr_offset\":\"0\"}).toString();\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconst result = await response.json();\nconsole.log(result);\nawait writeFile(\"stdout.bin\", Buffer.from(result.stdout_b64, \"base64\"));\nawait writeFile(\"stderr.bin\", Buffer.from(result.stderr_b64, \"base64\"));"}]}},"/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\nRequires the `viewer` role or higher.","operationId":"listWindows","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.windows()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/computers/{id}/windows/{window}":{"post":{"summary":"Act on a window","description":"Focus, raise, minimize, maximize, close, move or resize one window. 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.\n\nRequires the `member` role or higher.","operationId":"actOnWindow","tags":["Control"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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`, `close`, `move` or `resize`."},"x":{"type":"integer","description":"For `move`. Both x and y, or neither — one coordinate is not a position."},"y":{"type":"integer","description":"For `move`."},"width":{"type":"integer","description":"For `resize`. Both width and height, or neither."},"height":{"type":"integer","description":"For `resize`."}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"504":{"description":"The guest accepted a window action but did not report its result before the deadline. The action may already have happened, so this is an uncertain outcome rather than permission to repeat it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    window = \"0x3200007\"\n    body = json.loads(\"{\\\"action\\\":\\\"focus\\\"}\")\n    c = client.computers.get(id)\n    result = c.window_action(window, \"focus\")\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"vm-a1b2c3d4e5f6\";\nconst window = \"0x3200007\";\nconst body = {\"action\":\"focus\"};\nconst c = await client.computers.get(id);\nconst result = await c.windowAction(window, \"focus\");\nconsole.log(JSON.stringify(result, null, 2));"}]}},"/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`. 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 `xclip` in the guest, which you can do yourself since you have root there, or to create a new computer. 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.clipboard()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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` 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 `xclip` in the guest, which you can do since you have root there, or create a new computer. 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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 a 409, and so is one another agent run is already driving.\n\nIt runs on YOUR model key, which this platform never stores and never bills you for. Send it as the `X-Model-Key` header — without it the call is refused, whatever your API key.\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.\n\nEVERY ACTION THE LOOP TAKES SPENDS YOUR RATE BUDGET, the same budget your own calls draw on and at the same price — a click costs what an input plus a screenshot costs, because that is what it is. 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, 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, a computer that is not running, and a computer another run is already driving all arrive without one. The run’s opening computer lookup forwards 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."},{"name":"X-Model-Key","in":"header","required":true,"schema":{"type":"string"},"description":"Your own Anthropic key, e.g. `sk-ant-...`. Never stored, never metered."}],"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, if you want to steer how it works."},"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. So this bounds the WORK, and bounds your Anthropic spend only loosely — budget from your own key, not from this number. A run that reaches it ends with `stop: \"max_steps\"` and the work already done to the desktop stands."},"model":{"type":"string","description":"An Anthropic model id. Defaults to the one this platform picks."},"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`."}},"required":["prompt"]}}}},"responses":{"200":{"description":"An SSE event log of what the loop did, or one JSON body when `stream` is false.","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\":\"req_0123456789abcdef0123456789abcdef\",\"error\":\"The guest agent stopped responding after the initial screenshot.\",\"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\":\"Captured the initial screen\"}]}\n\n"}}},"application/json":{"schema":{"type":"object"},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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\" \\\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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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: process.env.X_MODEL_KEY!});\nconsole.log(JSON.stringify(result, null, 2));"}]}},"/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` rule as `POST /computers/{id}/agent`.\n\n`computer_id` on the body is the one addition to the shape, and there is nowhere else to put it: this endpoint has no computer in its path. Everything else reads the way an OpenAI client already writes it — the prompt is the last `user` message, and any `system` messages become the system prompt.\n\nTWO THINGS DIFFER FROM THE ROUTE ABOVE, both 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`. And `model` is **not** ignored: it is passed to Anthropic as written, so an OpenAI client left on its default will send something like `gpt-4o` and get a model error back. Send an Anthropic model id, or omit the field.\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"],"parameters":[{"name":"X-Model-Key","in":"header","required":true,"schema":{"type":"string"},"description":"Your own Anthropic key, e.g. `sk-ant-...`. Never stored, never metered."}],"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."},"model":{"type":"string","description":"An **Anthropic** model id, passed through as written. Not ignored, and not translated from an OpenAI name — `gpt-4o` reaches Anthropic and fails. Omit it to take our default."},"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."},"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`."}},"required":["computer_id","messages"]}}}},"responses":{"200":{"description":"A chat completion, or an SSE stream of them.","content":{"application/json":{"schema":{"type":"object"},"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\":\"Stream interrupted\",\"code\":502},\"request_id\":\"req_0123456789abcdef0123456789abcdef\"}\n\ndata: [DONE]\n\n"}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","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":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/OpenAIError"}]}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIError"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}}},"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\" \\\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.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"chat\" + \"/\" + \"completions\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"], \"X-Model-Key\": os.environ[\"X_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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"chat\" + \"/\" + \"completions\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey, \"X-Model-Key\": process.env.X_MODEL_KEY!, \"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());"}]}},"/computers/{id}/files":{"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\nRequires the `member` role or higher.","operationId":"uploadFile","tags":["Files"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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);"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"files\" + \"/\" + \"list\"\nurl += \"?\" + urlencode({\"path\":\"/home/user\"})\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"files\" + \"/\" + \"list\");\nurl.search = new URLSearchParams({\"path\":\"/home/user\"}).toString();\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}},"/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\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"],"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":"include","in":"query","required":false,"schema":{"type":"string","enum":["unfinished"]},"description":"Widen the listing to snapshots whose deletion began and did not finish — 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."}],"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}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.snapshots.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/computers/{id}/snapshots":{"get":{"summary":"Get what a computer holds","description":"A count, a byte total, and the fingerprint naming that exact set. NOT a listing of the snapshots themselves — that is `GET /snapshots`, and the two answer different shapes.\n\nRead this before an irreversible delete: the `fingerprint` is what makes a snapshot purge binding, and it is not something you can compute yourself from the listing.\n\nRequires the `viewer` role or higher.","operationId":"getSnapshotHoldings","tags":["Snapshots"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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":"cb391fc23c8f82bb"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.snapshot_holdings()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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\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\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, a disk still being copied, a capture of this computer already running, a memory snapshot of a computer that is not running, an allowance that will not stretch. A `202` means the capture started. 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Defaults to a generated one."},"memory":{"type":"boolean","description":"Include the running session. The computer must be running."}}}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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\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.\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, and it restores the disk and boots fresh. 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"responses":{"200":{"description":"Restored.","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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"snap-c1d2e3f4a5b6\"\n    result = client.snapshots.restore(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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\nRequires the `member` role or higher.","operationId":"cloneSnapshot","tags":["Snapshots"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Name for the new computer."}}}}}},"responses":{"201":{"description":"The new computer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputerConnect"},"examples":{"success":{"summary":"Illustrative successful response.","value":{"id":"vm-c3d4e5f6a7b8","name":"from-snapshot","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-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}}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"snap-c1d2e3f4a5b6\"\n    name = \"base\"\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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, baseUrl: process.env.MANDALA_BASE_URL || \"https://app.mandala.computer/api/v1\"});\nconst id = \"snap-c1d2e3f4a5b6\";\nconst name = \"base\";\nconst body = {\"name\":\"from-snapshot\"};\nconst result = await client.snapshots.clone(id, body.name);\nconsole.log(JSON.stringify(result, null, 2));"}]}},"/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\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` for the id and WAIT FOR THE ROW TO GO. Its absence is the deletion having finished; there is no state that means \"deleted\".\n\nA ROW THAT STAYS IS ONE THAT STALLED, and it is the opposite polarity to a failed capture, which leaves no row at all. Add `?include=unfinished` to `GET /snapshots` to see it: once the daemon has detached the dependents it 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. The daemon retries these itself.\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.\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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"responses":{"202":{"description":"The deletion, accepted — the row that goes when it has finished.","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":"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"snap-c1d2e3f4a5b6\"\n    result = client.snapshots.delete(id, wait=False)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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\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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.schedule()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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\nRequires the `member` role or higher.","operationId":"setSchedule","tags":["Schedules"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"hour":{"type":"integer","description":"0–23, in `tz`."},"minute":{"type":"integer","description":"0–59."},"tz":{"type":"string","description":"IANA zone name, e.g. `Europe/London`. Defaults to UTC."}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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.\n\nRequires the `member` role or higher.","operationId":"deleteSchedule","tags":["Schedules"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}},"503":{"description":"A hypervisor could not be reached, so this could not be answered honestly. Nothing has been lost — retry shortly. 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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"vm-a1b2c3d4e5f6\"\n    c = client.computers.get(id)\n    result = c.clear_schedule()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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\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"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.snapshots.retention()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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\nRequires the `viewer` role or higher.","operationId":"getUsage","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."},{"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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.usage.read()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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"],"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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    result = client.webhooks.list()\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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 `GET /computers/{id}/events` 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 socket with `since=` that cursor. `computer` says which machine, and every subscription may receive events from many.\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 to one endpoint 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.\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.\n\nRequires the `member` role or higher.","operationId":"createWebhook","tags":["Webhooks"],"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."},"description":{"type":"string","description":"Free text for your listing, up to 200 characters."},"events":{"type":"array","items":{"type":"string"},"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"},"description":"Computer ids to deliver for, up to 64. 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://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","secret":"whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"402":{"description":"Your plan refused this: a quota, or a payment that has not gone through.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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    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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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.\");"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.get(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]},"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; `enabled: false` stops deliveries and records that you chose to.\n\nRequires the `member` role or higher.","operationId":"updateWebhook","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or 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."},"description":{"type":"string","description":"Free text for your listing, up to 200 characters."},"events":{"type":"array","items":{"type":"string"},"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"},"description":"Computer ids to deliver for, up to 64. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.delete(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"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.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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.\");"}]}},"/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.\n\nRequires the `member` role or higher.","operationId":"testWebhook","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.test(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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 for seven days; 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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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.4.0","source":"# Python 3.10+: pip install mandala-computer==0.4.0\nimport json, os\nfrom pathlib import Path\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 = \"whk-e1f2a3b4c5d6e7f8\"\n    result = client.webhooks.deliveries(id)\n    print(result)"},{"lang":"TypeScript","label":"TypeScript — Mandala SDK 0.4.0","source":"// Node.js 22+: npm install mandala-computer@0.4.0\nimport { Client } from \"mandala-computer\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nconst client = new Client({apiKey: process.env.MANDALA_API_KEY, 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));"}]}},"/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, and each key reaches the computers of every account where you are an owner or member. 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\nRequires the `viewer` role or higher.","operationId":"listSSHKeys","tags":["SSH"],"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}]}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"ssh-keys\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"ssh-keys\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]},"post":{"summary":"Add an SSH key","description":"Registers a public key to you. Accepted: Ed25519, ECDSA (P-256, P-384, P-521), their FIDO security-key forms (`sk-…@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 written into every computer with SSH on, on every account where you are an owner or member, within moments. The answer does not wait for that.\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"],"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."},"name":{"type":"string","description":"A label, up to 60 characters. Defaults to the comment on the line."}},"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}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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, restoring onto a computer that is gone.\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, `unsupported` means this computer cannot do it at all. 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\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 — one move runs per account, and `GET /moves` says which.\n\nAnd the terminal socket refuses twice over: `resume_required` on a suspended computer, which clears when you start it and not before, and a 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; caller IDs are ignored.","schema":{"type":"string"}}}},"413":{"description":"What you sent, or asked for, is past the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"ssh-keys\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"], \"Content-Type\": \"application/json\"}\nbody = json.loads(\"{\\\"public_key\\\":\\\"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMxlN5MDRT9cXdHi871o7Ty3dKfNLt8mNmjSWtwv6DTw you@laptop\\\",\\\"name\\\":\\\"laptop\\\"}\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"ssh-keys\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey, \"Content-Type\": \"application/json\"};\nconst body = {\"public_key\":\"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMxlN5MDRT9cXdHi871o7Ty3dKfNLt8mNmjSWtwv6DTw you@laptop\",\"name\":\"laptop\"};\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());"}]}},"/ssh-keys/{id}":{"delete":{"summary":"Remove an SSH key","description":"Removes one of your keys. New connections with it are refused at once; it is removed from the computers it was written into within moments, and a session already open when you removed it goes on until it disconnects. Not available to a workspace-scoped API key (`403`).\n\nRequires the `member` role or higher.","operationId":"deleteSSHKey","tags":["SSH"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"ssh-keys\" + \"/\" + quote(\"sshk-a1b2c3d4e5f60718\", safe=\"\")\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"DELETE\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"ssh-keys\" + \"/\" + encodeSegment(\"sshk-a1b2c3d4e5f60718\"));\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"DELETE\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}},"/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"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"ssh\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"]}\nrequest = Request(url, headers=headers, method=\"GET\")\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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"ssh\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey};\nconst response = await fetch(url, {method: \"GET\", headers});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]},"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; 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\nRequires the `member` role or higher.","operationId":"setComputerSSH","tags":["SSH"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The computer, snapshot, build or webhook 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"401":{"description":"Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked). Provider 401 retains its provider error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Platform bearer credential missing; not an Anthropic key diagnosis.","value":{"error":"Authentication required","reason":"missing","request_id":"req_0123456789abcdef0123456789abcdef"}},"invalid":{"summary":"Platform bearer credential invalid; not an Anthropic key diagnosis.","value":{"error":"invalid API key","reason":"invalid","request_id":"req_0123456789abcdef0123456789abcdef"}},"revoked":{"summary":"Platform bearer credential revoked; not an Anthropic key diagnosis.","value":{"error":"API key revoked","reason":"revoked","request_id":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"WWW-Authenticate":{"description":"Platform API-key refusal: Bearer for missing, Bearer error=\"invalid_token\" for invalid/revoked. Provider refusals retain provider headers.","schema":{"type":"string"}}}},"403":{"description":"The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended. The message identifies the refusal; replacing a valid key alone does not restore authority.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"404":{"description":"No such computer, snapshot, or 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; caller IDs are ignored.","schema":{"type":"string"}}}},"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":"req_0123456789abcdef0123456789abcdef"}}}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}},"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 the size limit for this endpoint — a guest file transfer, a template document, or a clipboard. 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; caller IDs are ignored.","schema":{"type":"string"}}}},"429":{"description":"You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every response carries `RateLimit-Remaining` so you do not have to be refused to find out. The budget is spent in proportion to what a call costs rather than per request: reading one computer is the cheapest thing here, a screenshot or a listing that spans the fleet costs several times that, and launching a computer or starting an agent run costs many. 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"X-Request-ID":{"description":"Server-generated request correlation ID; caller IDs are ignored.","schema":{"type":"string"}}}},"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; caller IDs are ignored.","schema":{"type":"string"}}}}},"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 — direct HTTP (no released wrapper)","source":"# Direct HTTP: this operation has no wrapper in mandala-computer==0.4.0.\n# Python 3.10+; standard library only.\nimport json, os, hashlib, base64\nfrom pathlib import Path\nfrom urllib.request import Request, urlopen\nfrom urllib.parse import quote, urlencode\n\nbase_url = (os.environ.get(\"MANDALA_BASE_URL\") or \"https://app.mandala.computer/api/v1\").rstrip(\"/\")\nurl = base_url + \"/\" + \"computers\" + \"/\" + quote(\"vm-a1b2c3d4e5f6\", safe=\"\") + \"/\" + \"ssh\"\nheaders = {\"Authorization\": \"Bearer \" + os.environ[\"MANDALA_API_KEY\"], \"Content-Type\": \"application/json\"}\nbody = json.loads(\"{\\\"enabled\\\":true}\")\nrequest = Request(url, headers=headers, method=\"PUT\", 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.4.0.\n// Node.js 22+; no package required.\nimport { writeFile } from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\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 encodeSegment = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, c => \"%\" + c.charCodeAt(0).toString(16).toUpperCase());\nconst url = new URL(baseUrl + \"/\" + \"computers\" + \"/\" + encodeSegment(\"vm-a1b2c3d4e5f6\") + \"/\" + \"ssh\");\nconst headers: Record<string, string> = {Authorization: \"Bearer \" + apiKey, \"Content-Type\": \"application/json\"};\nconst body = {\"enabled\":true};\nconst response = await fetch(url, {method: \"PUT\", headers, body: JSON.stringify(body)});\nif (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);\nconsole.log(await response.json());"}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key from Settings, e.g. `com_...`."}},"schemas":{"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."},"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."},"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. 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, also sent in X-Request-ID. HEAD carries it only in the header."},"code":{"type":"string","description":"Optional route-specific detail. Execution reads use execution_unavailable for unknown/expired/inaccessible IDs and output_unavailable for a guest log that cannot be opened. Other routes may use other codes."},"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 is still inside the bounded boot window), `unavailable` (the computer is not running — **start it**; this never clears by waiting), `unsupported` (this computer cannot do it at all) or `revoked` (the authority this request arrived with no longer holds).\n\nThe first two 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"]},"OpenAIError":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/OpenAIErrorDetail"},"request_id":{"type":"string","description":"The server-generated X-Request-ID for this request."}},"required":["error"]},"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`."}}},"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"]},"route":{"type":"string","enum":["input","exec","window","clipboard"]},"action":{"type":"string","enum":["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."},"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"]},"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."},"os":{"type":"string","description":"`linux` or `windows`."},"desktop":{"type":"string","description":"The display protocol this computer’s desktop speaks: `wayland`, or absent for X11. 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."},"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"},"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`."}}},"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 stale by this long."}}},"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 `xclip` in the image, since they drive the guest’s own desktop session. A computer built from a golden that predates `xclip` 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\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."},"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 Windows one, or 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\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"}}}],"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."},"CreateResult":{"oneOf":[{"$ref":"#/components/schemas/ComputerConnect"},{"$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."}}},"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."}},"required":["id","name","public_key","fingerprint","key_type","created_at","last_used_at"]},"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. `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`."}}},"DeleteResult":{"type":"object","properties":{"ok":{"type":"boolean"},"snapshots_deleted":{"type":"integer","description":"How many snapshots went with the computer. `0` when you did not pass `snapshots=delete`, which is also what a computer with none answers — the two are the same number and this field does not distinguish them."}}},"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."}}},"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":"The source computer is gone. This decides which operation is available: `restore` puts the disk back on the source computer and cannot work without one, while `clone` builds a new computer from the snapshot alone and works fine."},"name":{"type":"string"},"kind":{"type":"string","description":"`disk` or `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 did not finish; 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."},"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":"How many snapshots this computer has."},"size_bytes":{"type":"integer","description":"What they add up to."},"fingerprint":{"type":"string","description":"Names the exact set the count and the size describe. Pass it back as `expect` on `DELETE /computers/{id}?snapshots=delete` to make the purge binding: the daemon refuses the sweep if the set has moved on since you were shown these numbers. You cannot reconstruct this string yourself, and it is the only interlock on an irreversible operation."}}},"Template":{"type":"object","properties":{"name":{"type":"string","description":"The short name of the template. Accepted as `template` on a create, and what a computer reports as its own `template`."},"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`, or absent for X11. 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, and an `os` that contradicts the `family` the document names."},"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, and comparing it across an edit tells you whether a rebuild is needed. 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 and where to compute it. 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"},"hour":{"type":"integer","description":"0–23, in `tz`."},"minute":{"type":"integer","description":"0–59."},"tz":{"type":"string","description":"IANA zone name. `UTC` when unset."}}},"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."}}},"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"]},"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"]},"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 US-layout keys, GTK Unicode composition, or both in order. 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`."}},"required":["ok"]},"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 reading 0 off it says the opposite of what happened."},"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"]},"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."}}},"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."}}},"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 the retention you set, 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."}}},"UsageComputer":{"type":"object","properties":{"id":{"type":"string","description":"The computer this line is for."},"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`."}}},"Webhook":{"type":"object","properties":{"id":{"type":"string","description":"`whk-` and sixteen hex characters."},"url":{"type":"string","description":"Where deliveries are POSTed. `https://` only."},"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 socket’s (`GET /computers/{id}/events`) 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 — see `disabled_reason` — and back to `true` by you with `PATCH`, which starts fresh."},"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. `null` while enabled."},"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"]},"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 pass as `since=` to the socket to read on from it."},"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)."},"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: `timeout`, `dns`, `refused`, `tls`, `redirect`, `address refused`, or `status NNN`. `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"]}}}}