---
title: "List computers"
method: GET
path: "/computers"
tags: ["Computers"]
---

# List computers

`GET /computers`

## Request

- Base URL: `https://app.mandala.computer/api/v1`
- URL: `https://app.mandala.computer/api/v1/computers`
- Auth: HTTP bearer

Every computer on the account, or — when the key is scoped to one workspace — every computer in that workspace. `workspace_id` narrows it to one workspace, or to the computers in none. No `vnc` on these rows; fetch one computer to get its desktop credentials.

Requires the `viewer` role or higher.

## Query parameters

- `allow_partial` '1' | 'true'
- `state` 'live' | 'unreachable' | 'deleting' | 'deleted' | 'lost'
- `workspace_id` string

## Response `200`

Your computers.

- Computer[]
  - `id` string: Stable identifier, e.g. `vm-a1b2c3d4e5f6`.
  - `name` string: The name you gave it.
  - `status` string: `running`, `stopped`, `suspended`, `building`, `build-failed` or `half-removed`. The last is a computer whose deletion stopped partway and took its disk with it: every call that needs a disk is refused on it, it will never start again, and deleting it again is what clears it. A client that treats an unknown status as stopped will offer a start this API always refuses, so read these as a closed set and everything outside it as not startable. A `running` computer can be paused by its host without its status changing. When the host is low on disk space, the guests writing fastest are paused, and when it is critically low every running computer on it is paused, including ones that wrote nothing. Screenshots, input and commands stall or fail meanwhile, and the computer resumes by itself once space recovers. A suspend or memory snapshot that fails can also leave it paused: always when the failure says “this computer remains paused because the host is low on disk space”, and other failures can, at any free space. While the platform holds it for disk-space recovery, start, restart, suspend and snapshot answer `503` (“waiting for disk-space recovery”) until it resumes by itself once that recovery can finish, and a stop discards the paused session. Nor does any computer start or resume on a host that is low on disk space: a start, a restart of a stopped computer and the resume that using a suspended computer sets off answer `503` (“this host is low on disk space…”), and a restore answers `503` too, until space on that host has been freed.
  - `os` string: `linux` or `windows`.
  - `desktop` string: The display protocol this computer’s desktop speaks: `wayland`, `x11`, or absent (X11, or a host too old to say); branch on `=== "wayland"`. Screenshots, input and `exec` behave identically either way. What differs on a Wayland computer is that a window `id` is the compositor’s address rather than an X window id — opaque and handed back the same way, but not comparable across the two — and that moving or resizing a window the compositor is TILING is refused rather than silently ignored, because a tiled window’s geometry belongs to the layout. Read it here rather than from the template: a computer keeps the image it was built from, and a template’s version can advance underneath it.
  - `template` string: The template this computer was built from.
  - `cpu` integer: Cores.
  - `ram_mb` integer
  - `disk_gb` integer
  - `running_ram_mb` integer: 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` string: 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` string: The workspace this computer is in. Absent when it is in none. It can change: a stopped computer can be moved to another workspace, or to none, with `PUT /computers/{id}/workspace` or from the dashboard.
  - `created_at` string: RFC 3339 timestamp.
  - `build` Build
    - `started` string: RFC 3339 timestamp.
    - `source` string: What the disk is being copied from.
    - `failed` string: Why the build failed, when it did.
  - `suspended` Suspended
    - `at` string: When the session was saved, RFC 3339. A resumed guest’s clock is set to the current time within a few seconds of the resume, through its guest agent; the desktop panel’s clock can show the saved time for up to a minute after that.
  - `idle_suspend_min` integer: 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` Schedule
    - `enabled` boolean, required: Whether a capture is taken in this window. A computer that is not running is captured only if its disk has changed since its last automatic snapshot; a stopped or suspended computer left unused takes no new automatic snapshot.
    - `hour` integer, required: 0–23, in `tz`.
    - `minute` integer, required: 0–59.
    - `tz` string, required: IANA zone name. `UTC` when unset.
  - `browser_proxy` BrowserProxy
    - `server` string, required: The proxy, as `http://host:port`, `https://host:port` or `socks5://host:port`, lower-cased as stored. The host is a name or an IPv4 address, the port is explicit, and there is nothing else in it: no path, and never a username or password — a URL carrying one is refused, because this setting is returned by every read of the computer; give an `http://` proxy its password with `credentials_secret_id` instead. `https://` is TLS to the proxy itself, for a proxy that listens for it. HTTPS sites are tunnelled end to end through an `http://` proxy too; `https://` also encrypts, between the browser and the proxy, which sites are visited and any plain-HTTP page. With `https://`, Chromium is given the proxy as it is, and Firefox — whose settings cannot name a TLS proxy — a proxy auto-config (PAC) script at `/etc/mandala/browser-proxy.pac` that its locked settings point at, answering `HTTPS host:port` and applying `bypass` exactly as Chromium does (with no DNS lookup: an address range matches only a URL whose host is an address), and it does not fall back to connecting directly when the proxy is down. Private, link-local (including the cloud metadata address), CGNAT, multicast and unspecified addresses are refused, since a computer cannot reach them; `127.0.0.1` and `localhost` are allowed and mean the computer itself, so a forwarder of your own running on it can be the proxy.
    - `credentials_secret_id` string: For an upstream that asks for a username and password: the id of a secret in your store whose value is `user:password` (a trailing newline is ignored). Only with an `http://` proxy for now, and not one on the computer’s own loopback. The computer holds the id, never the value, and the value reaches it one way only — the secret must be bound to the computer as a FILE, `{"secret_id": …, "file": "<name>"}` in its `secrets` (at create, or with `PUT /computers/{id}/secrets`), or the setting is refused with 400. So everything a bound secret does, this does: it is delivered as the computer starts, a computer’s first secrets can be bound only while it is stopped, a binding added to a running computer arrives at its next restart, and replacing the secret’s value reaches a running computer within seconds where its image takes live file updates (otherwise at its next start). While the setting names it, a change to the computer’s secrets that drops that file binding is refused with 409. The browsers are pointed at a forwarder on the computer’s loopback, `127.0.0.1:29380`, which reads `/run/mandala-secrets/user/files/<name>` on every connection and sends the upstream `Proxy-Authorization: Basic`; `bypass` applies as without credentials.
    - `bypass` string[]: Hosts the browser reaches directly, at most 64: `example.com` (that host), `*.example.com` or `.example.com` (its subdomains), an IPv4 address, a range such as `192.0.2.0/24` or `fd00::/8`, or `<local>` (names with no dot). Stored in a canonical spelling, which a read returns: trimmed and lower-cased, `.example.com` as `*.example.com`, a range masked to its network (`192.0.2.7/24` is `192.0.2.0/24`), and a bare IPv6 address as its `/128`; entries that are then the same are kept once. A range matches a URL whose host is an address in it, never a name that resolves into it. `localhost` and loopback addresses always bypass the proxy. So do link-local addresses (`169.254.0.0/16`, `fe80::/10`) in Chromium, and in Firefox with an `https://` proxy.
  - `egress_proxy` EgressProxy
    - `server` string, required: The proxy, as `http://host:port` (one that takes `CONNECT`), `https://host:port` (the same, spoken to over TLS) or `socks5://host:port`, lower-cased as stored. The host is a fully qualified name or a public IPv4 address, and the port is explicit; there is nothing else in it — no path, and never a username or password, which is refused because this setting is returned by every read of the computer (name a secret in `credentials_secret_id` instead). The computer’s HOST connects to it, so private, loopback, link-local (including the cloud metadata address), CGNAT, multicast, reserved and unspecified addresses are refused, and a name is checked again on every connection after it resolves: one that resolves to such an address is not dialled, and the computer’s connection fails. An `https://` proxy’s certificate is verified against the host’s public certificate authorities for the proxy’s name (or, for an address, the address); one that does not verify fails every connection, and nothing is sent directly.
    - `credentials_secret_id` string: For an upstream that asks for a username and password: the id of a secret in your store — in the computer’s workspace, or account-wide — whose value is `user:password` (at most 4096 bytes, no control characters; one trailing newline is ignored). A secret that is not there, or does not hold that, is refused with 400. Three more refusals, each changing nothing: 409 where delivering secrets is not switched on for the deployment, 409 when the secret’s value cannot be read on this control plane (replace its value), and 503 when secrets are not enabled at all. The computer never receives the value, and the secret is NOT bound to it: the computer’s HOST holds the value in memory and signs in to the proxy for it, with `Proxy-Authorization: Basic` on the `CONNECT` (`http://`, `https://`) or SOCKS5 username/password authentication (each part 1 to 255 bytes), and nothing inside the computer can read it. With `http://` or `socks5://` the credentials cross the network to the proxy in clear text; prefer `https://` when naming credentials. Replacing the secret’s value reaches the host within seconds, for connections opened after it; connections already open stay open. Deleting the secret removes the value from the host, and the computer’s connections are then refused until the setting names a secret that exists. While a running computer’s host does not hold the value — just after a create or a change, or after the host restarts — `egress_proxy_pending` is `true` and every connection the computer opens is closed; none is ever sent directly or without the credentials. Once a computer’s workspace is deleted, its egress credentials resolve account-wide only, so a proxy naming one of that workspace’s own secrets is held closed (`egress_proxy_pending` `true`) until it is saved again naming an account-wide secret.
  - `egress_proxy_pending` boolean: Present and `true` while a running computer’s `egress_proxy` names `credentials_secret_id` and its host does not hold that secret’s value yet — just after a create or a change, after the host restarts, or once the secret is deleted. Every connection the computer opens meanwhile is closed, never sent directly; it usually clears within seconds. It does NOT clear by itself once the computer’s workspace is deleted: its egress credentials then resolve account-wide only, so a proxy naming one of that workspace’s own secrets is held closed until it is saved again naming an account-wide secret. Never present on a computer that is not running.
  - `browser_proxy_pending` boolean: Present and `true` while a running computer’s guest does not have its `browser_proxy` yet — just after a create, a start or a change — or still has the files of one just removed. Wait for it to go away before starting a browser that must use the proxy. A browser already running when the setting changes applies it at its next start (Chromium usually within seconds). Never present on a computer that is not running: the setting is applied as it starts.
  - `state` string: 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` string: RFC 3339 timestamp of the answered delete. Present once `state` has been `deleted`.
  - `lost_at` string: RFC 3339 timestamp of the write-off. Present once `state` has been `lost`.
  - `unreachable` boolean: Present and `true` only on a row the platform served from its own record because the host holding this computer could not be reached. Such a row carries the computer’s identity — `name`, `os`, `template`, size, `workspace_id`, `created_at`, `state` — and nothing only its host knows: no `status`, no `resolution`. Its `state` is `unreachable`, or `deleting` when a delete is in flight for it. A listing with any such row carries `X-GC-Incomplete`, so you only ever see this after asking for a partial answer with `allow_partial=1`.
  - `secrets` object[]: The secrets bound to this computer — at create, or since with `PUT /computers/{id}/secrets` — as references: which secret, which revision of it, and the environment variable or file each lands under. Never a value. Present only on a computer that holds secrets; every field in this group is absent on one that does not, except `secrets_pending` while it is `true`. On any host that seals saved sessions a computer that holds secrets restarts — by this API or from inside it — and gets its secrets again as it comes back up; a restart of a stopped one is refused (`409`, `reason: "unavailable"`): start it instead, which delivers its secrets. It suspends — by hand or when idle — and can be memory-snapshotted. A disk snapshot restored onto it starts it with its secrets delivered afresh; a memory snapshot restored onto it resumes holding what it held when the snapshot was taken, and is refused if it was taken without sealing or the platform will no longer open it (see `POST /snapshots/{id}/restore`). Taking disk snapshots, cloning, moving between hosts and deletion work as usual, and a clone starts with no secrets bound, unless it is a fork of one of its memory snapshots made with `inherit_secrets: true`.
    - `secret_id` string, required: The secret, as `GET /secrets` lists it.
    - `revision_id` string, required: The revision last delivered to this computer, or the latest when it was bound if none has been delivered since. Every start and restart delivers each secret's latest value and moves this to it.
    - `env` string: The environment variable the value lands under in the desktop session, for a secret bound as one.
    - `file` string: For a secret bound as a file instead: its name under `/run/mandala-secrets/user/files`.
  - `secrets_generation` integer: How many delivering starts this computer has had. Each cold start of a computer that holds secrets is one generation; the receipt below names the generation it is for, so a receipt whose generation is behind this one is from an earlier start.
  - `secrets_applied` object: The receipt that the bound values reached this computer’s desktop session: which start, when, and which revision of each secret. This is the confirmation — a start that was taken but whose delivery did not land has no receipt for its generation and `secrets_error` says why. Absent before the first delivery.
    - `generation` integer, required: The delivering start this receipt is for.
    - `applied_at` string, required: RFC 3339 timestamp: when the values reached the desktop session.
    - `revisions` object, required: Secret id to the revision id that was delivered, for every binding.
  - `secrets_error` string: Why the last delivering start was stopped, in one fixed sentence naming the class of failure and never a value. A start whose delivery fails is stopped by the host and stays stopped until you start it again; this is the reason. Cleared when the next delivering start begins. Absent when the last delivery landed or none has been tried.
  - `secrets_pending` boolean, nullable: Whether this computer’s desktop is missing a change to its secrets that a restart would deliver: a binding, an env name or file, or a newer value it does not hold — or, once every secret has been removed, whether its running or saved session still holds the values it had. Compared against the receipt, so it clears once any delivery lands — a start, a restart from anywhere, a reboot inside it, or a secret file replaced while it runs — and stays after one that failed. A value replaced for a secret bound as a file is replaced live on a running computer, and is pending only if that did not land. One bound as an environment variable is the same on an image that hands new shells the current values: once they see it, it is not pending, though programs already running — the desktop session among them — keep the value they started with until a restart; on any other image it waits for a restart. A change that removed a secret, or moved one to another revision, stays pending until a restart either way, since the running session may still hold the old value. Only ever true of a running or suspended computer: a suspended one resumes with the values it had, and a stopped one gets everything current when it starts, so it is never pending. `null` when it could not be checked — the current values could not be read, or the computer’s last delivery predates the record of the names it was made under and it has been rebound since; a restart makes it known. A delivery still on its way is not pending — wait on `secrets_delivering` for that. Present whenever `secrets` is, and on a computer with no secrets bound only while true.
  - `secrets_delivering` boolean: Whether values are on their way into this computer’s desktop right now. A computer comes back `running` a few seconds before its secrets land, and a command run in between sees them unset; after a create, a start or a restart, wait for this to be `false` before relying on them. Also true from the moment a restart, or a reboot inside the computer, clears them until they are delivered again, and while a replaced value is written to a file. Reported by the computer’s host, which is doing the delivering. False on a computer that is not running and after a delivery that failed (`secrets_error` says why, and the computer is stopped). After a restart or a reboot it can also go back to `false` with nothing delivered and no `secrets_error`: that is a computer that has not come back up three minutes after it, and its secrets are not in it — restart it, or stop and start it. While it reads `true` only because a restarted computer has not come back up yet, a further restart is accepted, since that is the way out of one that does not; once the delivery itself has begun, a restart answers 409 until it lands. Present whenever `secrets` is.

## Other responses

- `400`: The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.
- `401`: Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).
- `403`: The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: "revoked"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.
- `404`: No such resource in your scope — a computer, snapshot, secret, webhook, SSH key, API key, workspace, operation and so on — or no such endpoint; a confirmed missing guest file is no such file in the guest.
- `405`: This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.
- `429`: You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: "contention"` (see `POST /snapshots/{id}/clone`).
- `500`: The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.
- `503`: Something this depends on — a hypervisor, a store, or a host’s capacity — could not answer or take this right now. A read can be retried shortly. A change answered `503` may or may not have happened — a failure after the request was sent is answered the same way, with or without a warning in the message — so read the current state before retrying anything that is not safe to repeat, such as a create or a command. On the collection reads, `allow_partial=1` accepts a knowingly short answer instead.

---

[API](https://skmtc.dev/mandala/apis/mandala-computer-api.md) · [All operations](https://skmtc.dev/mandala/apis/mandala-computer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mandala/apis/mandala-computer-api/revisions/2afbb87fa703?raw)
