---
title: "Take a snapshot"
method: POST
path: "/computers/{id}/snapshots"
tags: ["Snapshots"]
---

# Take a snapshot

`POST /computers/{id}/snapshots`

## Request

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

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.

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

MANUAL CAPTURES COUNT STORED BYTES AND CAPTURES ALREADY IN PROGRESS across the account. A capture can start while usage is below the allowance even if its eventual size crosses it; later captures are refused with 402 until usage is back under the allowance. A capture requested while another manual capture on the account is still being admitted answers 503 — retry. If acceptance cannot be confirmed, check the snapshot list before retrying. Admission also answers `503` (“Snapshot admission could not be confirmed…”) whenever any host in the fleet cannot confirm what snapshots it holds — one that is down or not answering, one whose snapshot storage is failing, or one running software too old to confirm it — even a host that holds nothing of yours; it lasts as long as that host’s state does. Automatic (scheduled) captures count toward the allowance too, so they can be what brings an account to the `402` — and a scheduled capture can be skipped, rather than taken, when the allowance is full, or on a new account or a computer newly placed on its host until the next complete hourly measurement of the allowance reaches that host. See `PUT /computers/{id}/schedule`.

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

ONE CAPTURE OF A COMPUTER AT A TIME, AND IT LASTS UNTIL THE UPLOAD. A capture holds its computer until its upload to backup storage has finished, which is later than when it stops reading `capturing`. Another capture of the SAME computer in that window — yours, or a scheduled one — is refused with `409` and `reason: "contention"`; retry, and it is accepted once the upload has finished, which is ordinarily when the previous row reads `durable`. Captures of other computers are not held up by it.

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

EVERYTHING THAT CAN REFUSE A CAPTURE IS REFUSED HERE, before the `202`: no such computer (`404`), a disk still being copied (`409`), a computer being deleted (`409`), one that has not yet finished taking on an identity of its own, such as a new computer that has never been started (`400`; start it, let it come up, and try again), a host with no snapshot storage configured (`400`), a capture of this computer still holding it (`409`, `reason: "contention"`; see above), a memory snapshot of a computer that is not running (`409`, `reason: "unavailable"`; start it), an allowance that will not stretch (`402`), a computer the platform is holding for disk-space recovery after a suspend or memory snapshot failed (`503`, “waiting for disk-space recovery”; wait, or stop it to discard the paused session), and three that only a memory snapshot of a computer holding secrets meets: its secrets still being delivered (`409`, `reason: "contention"`, and worth retrying — it clears in a moment; `reason: "unsupported"` instead for a session they were never delivered to and that nothing is delivering to now, which a stop and a start clears; for such a session, `reason: "contention"` when its host could not read whether it is running just now, worth sending again shortly, and `reason: "unsupported"` when its host cannot read its process until support repairs the host), a session that may still hold secret values it is no longer bound to (`409` until the computer restarts), and a host that does not seal saved memory (`409`; a disk snapshot is the way out). A `202` means the capture started, not that it will land: a capture waits its turn behind others on the same host, and when that turn comes a memory capture checks again that the computer is running and the three secrets refusals above, and, on a computer holding secrets, that its guest says its secrets reached the boot it is running and that it does not reboot during the capture. So one already accepted can still end if the computer stops, a delivery starts or its bindings change while it waits, and on such a computer if it reboots or its guest does not answer. Any capture can also end after the `202` when the host does not have enough unreserved disk space for the copy, and a memory capture that fails while the host is low on disk space can leave the computer paused, still reading `running` (see `status` on Computer). A capture that then fails leaves no snapshot and no row — the `capturing` row disappears and nothing takes its place, which is how you tell that from one still running.

Requires the `member` role or higher.

## Path parameters

- `id` string, required

## Request body

- object
  - `name` string: Defaults to the computer’s name, a space, and the UTC time the copy was taken as `YYYY-MM-DD HH:MM`. When you do not send one, the `capturing` row this call returns has an empty `name` until the capture lands.
  - `memory` boolean: Include the running session. The computer must be running, and one holding secrets must have finished receiving them, must not have had its bindings changed while it ran, and must be on a host that seals saved memory — each a `409` at admission otherwise, and a capture already accepted can still end afterwards for the same reasons; see above.

## Response `202`

The capture, accepted — a placeholder row carrying the id the snapshot will have.

- 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

- `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).
- `402`: Your plan refused this: a quota, or a payment that has not gone through. On an agent run it can instead be `Insufficient AI credits` with `top_up_url` (a run funded by the account’s AI credits), or the model provider’s own billing refusal of your `X-Model-Key`, relayed.
- `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.
- `409`: Refused for the state something is in — resizing a computer that is running, running a command on one that is stopped, taking a snapshot while another capture of it is running. WHETHER 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. Where the platform can say which it is, it sends `reason` beside `error` — one word, meant to be switched on rather than read: `contention` and `starting` clear on their own, `unavailable` means the computer is not running and only starting it helps, `running` means the computer is running and only stopping it helps (a resize), `unsupported` means waiting will not change the answer — this computer cannot do it at all, or it needs another step first, which the message names (a stop, or removing its secret bindings, for example) — `exists` means a create-only upload found its path taken, `name_taken` means a secret of that name is already in that scope, `stale_revision` means a secret changed since the `revision_id` you sent was read (or a computer’s bound list since the `version` you sent), and `stale_capture` means the screenshot capture you named is no longer held: a newer one replaced it, or its host’s daemon restarted or the computer moved. `starting` covers the two minutes after a start or a restart, and after a reboot from inside the guest on a computer last cold-started recently enough to report one (a stop and a start gives an older one that); a guest agent that is still silent after that is a `502`. Match on that and never on the sentence, which is prose and is rewritten. **Absent means no classification was given** — treat it, and any word you do not recognise, as no answer, because not every refusal here has one yet. It is what the guest routes answer: the clipboard on a stopped computer used to be a 409 indistinguishable from a clipboard whose selection was claimed for an instant, and a blanket retry loop spun against the first until its deadline. Every 409 that clears on its own carries `contention` or `starting`: a host busy with another build or with no room for one, an operation already running on the same computer or snapshot (a capture, a copy, a delete, a restore or clone reading it, another agent run), a lock another request holds, a move or a delivery settling. So a 409 with no word, or with any other word, is not one that waiting fixes: read the sentence for the step it names. The ones worth knowing by name, because each has a next step that is not "wait": A 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. The move itself then refuses for the things it re-decides at the moment it runs, and only one of those is worth waiting on: a computer that is running or suspended has to be stopped or resumed-and-stopped by you, a size that fits where it already is wants an ordinary resize instead, and a region with nowhere to put it wants a smaller size. `Another computer on this account is being moved right now` is the one that clears by itself, and it carries `reason: "contention"` — one move runs per account, and `GET /moves` says which. And the terminal socket refuses twice over: `reason: "unavailable"` with `resume_required: true` on a suspended computer, which clears when you start it and not before, and `reason: "unsupported"` on a running computer whose hardware carries no terminal channel — one last started before interactive terminals existed — which needs a stop and a start. Not a restart, which resets the same machine, and not a retry, which never comes good.
- `413`: What you sent, or asked for, is past a size limit. A JSON request body is capped at 1 MiB unless the route states its own limit: `POST /computers/{id}/exec` takes up to 4 MiB, and some routes take less; guest file transfers, template documents and the clipboard have limits of their own. The message names the limit that applied.
- `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)
