---
title: "List snapshots"
method: GET
path: "/snapshots"
tags: ["Snapshots"]
---

# List snapshots

`GET /snapshots`

## Request

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

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.

Uncertain fleet computer presence can require `allow_partial=true` even when snapshot rows were read; those rows carry `computer_unreachable` instead of a confirmed orphan status.

CAPTURES 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.

CHECK `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.

Requires the `viewer` role or higher.

## Query parameters

- `allow_partial` '1' | 'true'
- `include` 'unfinished'

## Response `200`

Your snapshots — every one you can act on, plus unfinished deletions if you asked for them.

- Snapshot[]
  - `id` string
  - `computer_id` string: The computer these bytes came from. May name a computer that no longer exists.
  - `computer_name` string: 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` boolean: True when no host that answered lists the source computer, and every host that did not answer is one the platform’s records show holding none of this account’s computers. False when a host lists it. Omitted when that cannot be established, with `computer_unreachable: true`. Also omitted on the row `DELETE /snapshots/{id}` answers with, which does not work it out; read it from `GET /snapshots`.
  - `computer_unreachable` boolean: True when source computer presence cannot be established. Snapshot bytes may still be available; orphaned is omitted.
  - `restore_available` boolean: The snapshot has a usable copy on the current source computer host. False for old-host copies; clone remains available when the snapshot itself is reachable.
  - `name` string
  - `kind` 'disk' | 'memory': `disk` or `memory`.
  - `state` 'capturing' | 'pending' | 'durable' | 'deleting': Where these bytes have got to. `capturing` — the copy is still being taken, and this row is a placeholder rather than a snapshot: restore, clone and delete all 404 on it, though its id is already the id it will keep. `pending` — it is on the host and usable, and is being pushed to backup storage. `durable` — it is in backup storage too, which is the state your plan’s retention (`GET /retention`) ages out. `deleting` — a deletion began and has not finished, whether still under way or stalled; you only see these if you asked for them with `include=unfinished`. Poll 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. A capture holds its computer until its upload to backup storage has finished, not just until it leaves `capturing`: a new capture of the SAME computer is refused (`409`, `reason: "contention"`) while that upload runs, which is ordinarily for as long as this row reads `pending`, and is accepted once it reads `durable`. Restore, clone and delete all work from `pending`.
  - `size_bytes` integer
  - `created_at` string: RFC 3339 timestamp.
  - `os` string
  - `template` string
  - `cpu` integer
  - `ram_mb` integer
  - `disk_gb` integer
  - `resolution` string: The screen the capture was taken at, and what a clone of it comes up as.
  - `auto` boolean: 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` boolean: This snapshot is a link in a chain rather than a full copy, which is why it bills smaller.
  - `unreachable` boolean: As on Computer — a row from the placement cache, with nothing else on it.

## Other responses

- `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.
- `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)
