Authentication
The resource requests documented here carry an API key as a bearer token. Human login uses the separate device authentication bootstrap protocol. Keys are created in Settings, through explicit browser approval (device login), or with POST /api-keys by a key its holder allowed to manage keys (the key’s menu under Settings → Credentials → API keys → Allow managing keys) and whose holder is a member or owner, and are shown once — copy it when it appears.
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body "${base_url%/}"'/computers' \
-H "Authorization: Bearer $MANDALA_API_KEY"A key acts as one account, with a role. 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 rather than an account-wide one. Each endpoint in the reference says which role it needs.
The access tokens the hosted MCP server at https://app.mandala.computer/mcp signs in with are not API keys: sent here directly, one is refused with 401 (reason: "invalid"). Use an API key.
HTTP conventions
HTTP 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.
OPTIONS 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.
Listings: there is no single paging scheme, and most listings have none: they answer everything in one response and take no page parameters. Three page. GET /operations and GET /computers/{id}/activities take an opaque cursor and answer next_cursor; GET /credits takes an integer before and answers next_before. Send next_cursor or next_before back until it is null. The activities changes_cursor is not a page cursor and is never null: it is a feed position, read on from with changes=1. GET /computers/{id}/signals is a feed rather than a listing: send the cursor it answered as since. A few listings are capped rather than paged, and say so: GET /webhooks/{id}/deliveries answers the newest hundred, and GET /computers/{id}/files/list marks a cut-short directory truncated: true.
Application responses carry a server-generated X-Request-ID. Finite HTTP error objects and application SSE error frames also carry request_id; OpenAI errors retain their nested error object and streaming [DONE] terminator. Caller X-Request-ID is not accepted as the correlation identity. Successful binary/SSE content is unchanged. HEAD errors carry correlation only in the header. Non-JSON, malformed, aborted, oversized (over 1 MiB), or stalled (over 2 seconds of error-body reading) upstream errors use a small correlated envelope with the original status. Infrastructure failures outside the application may have a different shape. The desktop, terminal and browser WebSocket handshakes are an exception: a refusal from the platform’s edge, before the handshake reaches the computer’s host, carries no X-Request-ID and no request_id.
Idempotency-Key is honoured only on the routes that document it (the lifecycle operations). Every other route ignores the header: it is neither validated nor deduplicated, so a retried POST /webhooks, POST /api-keys, POST /secrets, POST /workspaces, POST /ssh-keys or snapshot capture does the work again — a second webhook, API key, secret, workspace or snapshot, or a refusal because the first one already holds that name or SSH key. List before retrying a create whose answer you did not see.
A path with a trailing slash is redirected (308) to the same path without it before the API sees the request, so that redirect carries no X-Request-ID; send paths without one.
Platform 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.
The 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.