Capturing a computer, and building from a capture.
Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.
GET/snapshots
List snapshotsviewer+
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.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
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%/}"'/snapshots' \
-H "Authorization: Bearer $MANDALA_API_KEY"
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client
with 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:
result = client.snapshots.list()
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const result = await client.snapshots.listWithStatus();
console.log(JSON.stringify(result, null, 2));
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 snapshot on a host that cannot be reached is listed as its id and unreachable: true, and nothing else; a key scoped to a workspace gets no such rows, only the short answer. A row whose source computer cannot be found for certain carries computer_unreachable: true and no orphaned.
include
string
Widen the listing to snapshots whose deletion began and has not finished — 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. Poll with it to learn when a deletion has finished: the row goes then.
Response
Your snapshots — every one you can act on, plus unfinished deletions if you asked for them.
Response fields
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.
GET/computers/{id}/snapshots
Get what a computer holdsviewer+
Complete, scoped fleet holdings for this computer, including physical duplicates, captures and unfinished deletions. Returns an opaque fingerprint for DELETE consent and current computer presence. A host that cannot be asked, or has no configured store, is left out only when nothing of yours can be on it; otherwise this returns 503 instead of a total. That 503 carries found (count, size_bytes): what the hosts that answered hold, a lower bound with no fingerprint, so it can never confirm a purge. A computer being moved between hosts returns 409 with reason: "contention"; wait for the move to finish. Two hosts listing it with no move under way returns the same 409 with no reason: a copy support has to settle. A changed fleet returns 409 with no reason; refresh the confirmation. Works after computer deletion when authoritative scoped ownership or scoped snapshot evidence remains. The presentation list GET /snapshots may merge duplicate IDs and cannot reconstruct this fingerprint.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials. SDK setup first GETs the computer. Python start/stop/suspend/restart also refresh it with another GET; TypeScript refreshes only if the action returns an acknowledgement.
curl
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/vm-a1b2c3d4e5f6/snapshots' \
-H "Authorization: Bearer $MANDALA_API_KEY"
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client
with 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:
id = "vm-a1b2c3d4e5f6"
c = client.computers.get(id)
result = c.snapshot_holdings()
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "vm-a1b2c3d4e5f6";
const c = await client.computers.get(id);
const result = await c.holdings();
console.log(JSON.stringify(result, null, 2));
405 Unsupported method on this public path; HEAD returns these headers without a body.
HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, POST, OPTIONS
{
"error": "method not allowed",
"request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}
Path parameters
idrequired
string
The computer id.
Response
The summary.
Response fields
count
integer
Physical snapshot copies across the complete fleet, including duplicate IDs on different hosts, captures, and unfinished deletions.
size_bytes
integer
The total currently reported bytes of those physical copies.
computer_present
boolean
Whether the source computer is currently present. This presence is bound into the fingerprint. A snapshot-only cleanup confirmation must require false.
capturing
integer
In-flight captures. Purge is refused while any remain.
deleting
integer
Copies with unfinished deletion work.
fingerprint
string
Opaque versioned fingerprint binding scope, computer presence, host lifecycle identities and the exact physical copies. Pass it back as expect on DELETE /computers/{id}?snapshots=delete; a changed inventory is refused before mutation.
POST/computers/{id}/snapshots
Take a snapshotmember+
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.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials. SDK setup first GETs the computer. Python start/stop/suspend/restart also refresh it with another GET; TypeScript refreshes only if the action returns an acknowledgement.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X POST "${base_url%/}"'/computers/vm-a1b2c3d4e5f6/snapshots' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"name":"before-upgrade","memory":true}'
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import json, os
from mandala_computer import Client
with 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:
id = "vm-a1b2c3d4e5f6"
body = json.loads("{\"name\":\"before-upgrade\",\"memory\":true}")
c = client.computers.get(id)
result = c.snapshot(**body, wait=False)
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "vm-a1b2c3d4e5f6";
const body = {"name":"before-upgrade","memory":true};
const c = await client.computers.get(id);
const result = await c.snapshot({...body, wait: false});
console.log(JSON.stringify(result, null, 2));
405 Unsupported method on this public path; HEAD returns these headers without a body.
HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: GET, HEAD, POST, OPTIONS
{
"error": "method not allowed",
"request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}
Path parameters
idrequired
string
The computer id.
Request body
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
The capture, accepted — a placeholder row carrying the id the snapshot will have.
Response fields
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.
POST/snapshots/{id}/restore
Restore a snapshotmember+
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.
A snapshot held by two hosts at once — a move part way through handing its history over — is served from the copy on the host its computer now lives on. One that cannot be attributed that way, such as an orphan, answers 409 until the handover finishes; nothing is changed.
A snapshot taken before its computer moved to another host (POST /computers/{id}/move) is carried to the new host after the move. Until it lands, restoring it answers 409 with reason: "contention", saying it is still being carried over; try again in a few minutes. If the carry failed, the 409, with no reason, says it is still on the host the computer moved from, and that does not clear on its own: clone it instead, or ask support to move it. Nothing is changed either way. Cloning the snapshot works while it is being carried, except while the new host is taking it over, when a clone can answer 409 as well; retry once the carry has finished.
IT 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. On a host without enough free disk space for it, a restore answers 503 before anything is touched. If space runs short after the disk has been replaced, the start at the end can still be refused with 503 (“this host is low on disk space…”), and the computer is left stopped on the restored disk: start it once space there has been freed. If that message adds that its paused QEMU could not be stopped, stop the computer before trying again.
A computer that holds secrets restored from a DISK snapshot gets them delivered afresh at that boot, with their current values, exactly as a start delivers them; the snapshot never carried them. This delivery has the same workspace-conflict refusals and recovery as POST /computers/{id}/start. One restored from a memory snapshot resumes holding what it held when the snapshot was taken. A memory snapshot taken without sealing is refused onto a computer that now holds secrets (409, reason: "unsupported"), and a sealed one with 400 and no reason when the platform will not open its saved memory for this computer, for example because a secret it held has since been deleted or moved out of the computer’s scope. Both are checked before anything is stopped, and then nothing is stopped or replaced. The saved memory is opened again when it loads, so a refusal that arrives only then — a secret deleted in the meantime, say — ends the restore after the disk has been replaced, leaving the computer stopped.
A 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: a clone resumes the session in a new computer at the captured shape — for a snapshot of a computer that held secrets only with inherit_secrets: true, and otherwise it is built from the disk and says so with memory_dropped: true — or, with memory: false, is built from the disk alone; one built from the disk lands stopped and boots fresh when you start it (see POST /snapshots/{id}/clone). Refused before anything is taken down, so a computer that gets this answer is untouched.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X POST "${base_url%/}"'/snapshots/snap-c1d2e3f4a5b6/restore' \
-H "Authorization: Bearer $MANDALA_API_KEY"
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client
with 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:
id = "snap-c1d2e3f4a5b6"
result = client.snapshots.restore(id)
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "snap-c1d2e3f4a5b6";
const result = await client.snapshots.restore(id);
console.log(JSON.stringify(result, null, 2));
405 Unsupported method on this public path; HEAD returns these headers without a body.
HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: POST, OPTIONS
{
"error": "method not allowed",
"request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}
Path parameters
idrequired
string
The snapshot id.
Headers
Idempotency-Key
string
Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so GET /operations?idempotency_key= finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers 409 idempotency_in_progress while the first is running, and the original answer — status and body, with Idempotent-Replayed: true — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a 200 carrying {operation_id, operation}, also with Idempotent-Replayed: true: operation is the call’s operation, or null when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (vnc, with its tokens and URLs): read the computer (GET /computers/{id}) for them. The same key on a different request is a 422 with code: "idempotency_key_reused", and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a 400 with code: "idempotency_key_invalid"; neither does anything. A call refused with a 4xx that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose 4xx says the computer was deleted — computer_deleted: true, or a purge result beside computer_deleted: false (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is succeeded. Except a 409 with reason: "contention" whose purge deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With computer_deleted: null, or false where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers 409 idempotency_in_progress until the platform gives up on it, then 409 idempotency_outcome_unknown. A 5xx answer that names an operation_id is an unknown outcome too: that is the operation reserved for it. From then on the same key answers 409 idempotency_outcome_unknown, naming that operation (not idempotency_in_progress): read the computer, or the operation. The operation stays pending until the platform gives up on it; if it never learns how the call ended, it becomes failed with error.code: "lost". Some 5xx answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a 503 from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, suspend_outcome: "not_started"). Those name no operation_id: nothing was done, so the key is released as a 4xx refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its 5xx is an unknown outcome like any other. A 5xx without an operation_id is not proof that nothing was done on its own: a call that records no operation (a PATCH that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any 5xx: it is carried out if its key was released, and answered 409 idempotency_outcome_unknown if not.
Response
Restored.
Response fields
okalways
boolean
Always true. A failure is a status code, not ok: false.
operation_id
string
This call’s operation, already succeeded: the step was done before the answer was sent. Read it with GET /operations/{id}. Absent, with the call still done, in the rare case the operation could not be recorded; a client must not treat its absence as a failure of the call.
Failures
400
Also a malformed Idempotency-Key, with code: "idempotency_key_invalid": empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.
409
With an Idempotency-Key, two more. code: "idempotency_in_progress", with reason: "contention", Retry-After and the operation_id: the call with this key is still being carried out — send it again shortly for its answer. code: "idempotency_outcome_unknown", with no reason: the platform did not hear how the call with this key ended (as after a 5xx answer to it that named an operation_id), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.
Also, for a computer that holds secrets restored from a disk snapshot, the secret refusals of POST /computers/{id}/start, with its words: contention while the computer is being moved to another workspace or a change to its secrets is still being saved, unsupported for a workspace change whose outcome was lost, bindings recorded in a workspace the computer is no longer in, a host list of bindings that does not match the recorded one, a bound secret that is gone or cannot be delivered, or secrets delivery not switched on for this deployment.
422
This Idempotency-Key was used within its 24 hours with a different request (code: "idempotency_key_reused"). Nothing was done; send this request under a new key.
503
Also a sealed session the platform could not open, seal or carry just then — the service that holds its key did not answer, or the host failed moving the saved session — which is the platform’s fault and not the request’s. Read the computer’s status, then send the restore again; it usually clears in a moment. Sent with an Idempotency-Key, sending it again under the same key is safe: when the host refused it before changing anything, its key was released and the answer names no operation_id, so the resend is carried out; when the answer names an operation_id, the outcome is unknown and the resend answers 409 idempotency_outcome_unknown — read the computer and that operation instead.
POST/snapshots/{id}/clone
Clone a snapshot into a new computermember+
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.
A snapshot held by two hosts at once — a move part way through handing its history over — is served from the copy on the host its computer now lives on. One that cannot be attributed that way, such as an orphan, answers 409 until the handover finishes; nothing is changed.
The answer arrives as soon as the new computer exists, with status: "building" while the snapshot is copied out — poll it, or wait on its operation_id. A clone BUILT FROM DISK — a disk snapshot, memory: false, or one answered with memory_dropped: true — lands STOPPED, and boots fresh when you start it; a caller waiting for running without starting it waits for ever. A memory fork resumes by itself once it is built.
A MEMORY snapshot is resumed: the new computer comes up running the captured session. Pass memory: false to build it from the snapshot's disk alone instead, with its own network identity, landing stopped — the way out when the saved session is what is broken.
A snapshot whose workspace has since been deleted is cloned into no workspace; only an account-wide key reaches it. Its saved session cannot be resumed there, so a clone of a memory snapshot that would resume it (memory absent, null or true) is refused with 409 and nothing is created. Send memory: false to build it from its disk instead; the new computer is in no workspace.
A memory snapshot of a computer that held secrets is resumed into a new computer only if you pass inherit_secrets: true: its saved memory holds the delivered values, so the new computer comes up holding the same credentials, bound to the same secrets at the revisions it holds, and gets current values at its next reboot. It lands in the same workspace, and is refused its session if any of those secrets has since been deleted or moved out of scope. Without it — or for a snapshot taken before this could be recorded — the clone is built from the disk and the response says so with memory_dropped: true; check for it before assuming the session came across.
A resumed copy is given an identity of its own before its network comes up: its own MAC and address, its name as hostname, a new machine ID and SSH host keys, and its own desktop password, so it runs beside its source. The session is otherwise untouched. If that cannot be done the copy is stopped rather than left running as its source; starting it boots its disk fresh.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X POST "${base_url%/}"'/snapshots/snap-c1d2e3f4a5b6/clone' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"name":"from-snapshot"}'
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import json, os
from mandala_computer import Client
with 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:
id = "snap-c1d2e3f4a5b6"
body = json.loads("{\"name\":\"from-snapshot\"}")
result = client.snapshots.clone(id, body["name"])
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "snap-c1d2e3f4a5b6";
const body = {"name":"from-snapshot"};
const result = await client.snapshots.clone(id, body.name);
console.log(JSON.stringify(result, null, 2));
405 Unsupported method on this public path; HEAD returns these headers without a body.
HTTP 405
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
Allow: POST, OPTIONS
{
"error": "method not allowed",
"request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}
Path parameters
idrequired
string
The snapshot id.
Headers
Idempotency-Key
string
Optional: a key of your choosing, 1 to 255 printable ASCII characters with no spaces (a UUID is ideal), that makes this call safe to send again after a lost answer. The call and its operation are recorded BEFORE it is carried out, so GET /operations?idempotency_key= finds it however the connection ended. For 24 hours, the same key with the byte-identical request (method, path, query and body, from a credential of the same scope) is not carried out again: it answers 409 idempotency_in_progress while the first is running, and the original answer — status and body, with Idempotent-Replayed: true — once it has finished. An answer too large to keep (over 1 MiB) is replayed instead as a 200 carrying {operation_id, operation}, also with Idempotent-Replayed: true: operation is the call’s operation, or null when there is none to show. Keys are kept per scope: a key used by a workspace's credential and the same key used by another workspace's, or by an account-wide one, are different keys, and neither is refused because of the other. A replayed body omits the desktop credentials the original carried (vnc, with its tokens and URLs): read the computer (GET /computers/{id}) for them. The same key on a different request is a 422 with code: "idempotency_key_reused", and a malformed key (empty, too long, outside printable ASCII, or the header sent twice) a 400 with code: "idempotency_key_invalid"; neither does anything. A call refused with a 4xx that did nothing releases its key, so it may be sent again under the same one. The exception is a delete whose 4xx says the computer was deleted — computer_deleted: true, or a purge result beside computer_deleted: false (it was deleted, then listed again): it happened, so its key keeps that answer and its operation is succeeded. Except a 409 with reason: "contention" whose purge deleted, queued and left in doubt nothing (the computer was already gone and copies in use refused the purge): nothing happened, so its key is released and the same request may be sent again under it. With computer_deleted: null, or false where the platform cannot confirm the computer was left in place, it is an unknown outcome: its key answers 409 idempotency_in_progress until the platform gives up on it, then 409 idempotency_outcome_unknown. A 5xx answer that names an operation_id is an unknown outcome too: that is the operation reserved for it. From then on the same key answers 409 idempotency_outcome_unknown, naming that operation (not idempotency_in_progress): read the computer, or the operation. The operation stays pending until the platform gives up on it; if it never learns how the call ended, it becomes failed with error.code: "lost". Some 5xx answers are refusals given before the call was sent anywhere — the platform could not check the account’s usage, or find a host for the call, for example — and some are a create or clone the host confirms it refused before building anything, such as a 503 from a host with no room left, and some are a start, restore or suspend the host confirms it refused before changing anything (a sealed session whose key could not be opened or whose transport failed first; on a suspend, suspend_outcome: "not_started"). Those name no operation_id: nothing was done, so the key is released as a 4xx refusal’s is, and the same call may be sent again under the same key. A host running older software cannot confirm such a refusal, so its 5xx is an unknown outcome like any other. A 5xx without an operation_id is not proof that nothing was done on its own: a call that records no operation (a PATCH that does not resize) names none, and neither does one whose operation could not be recorded. Sending the same call again under the same key is safe after any 5xx: it is carried out if its key was released, and answered 409 idempotency_outcome_unknown if not.
Request body
name
string
Name for the new computer. Defaults to the snapshot’s name followed by -restored. Cut to 64 characters, with control characters turned into spaces and the ends trimmed. A name left empty by that cleaning gets the default too.
memory
boolean
Memory snapshots only: false builds from the disk alone; it lands stopped and boots fresh when started. Defaults to true. Ignored for a disk snapshot. A memory snapshot whose workspace was deleted needs false; see above.
inherit_secrets
boolean
Consent to resuming a memory snapshot of a computer that held secrets: the new computer holds the same credentials. Ignored for a snapshot that holds none, and when memory is false.
Response
The new computer.
Response fields
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.
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.
The proxy this computer’s browsers are sent through — Chromium, Chrome where it is installed, and Firefox — as a managed policy each reads when it starts, locked so the browser’s own settings cannot override it. Only the browsers: exec, a terminal and every other program reach the network directly. Absent when none is set. Set at create or with PATCH /computers/{id}; a clone does not inherit it.
The proxy ALL of this computer’s outbound TCP is sent through, taken on its host at the computer’s network port, so nothing inside the computer is configured and nothing there can opt out: exec, terminals, package managers and browsers alike. Each connection is tunnelled to the host and port it was going to, by name where the connection says one (the TLS server name, or an HTTP Host) so the proxy resolves it, and otherwise by address. It FAILS CLOSED: when the proxy is down or refuses, the connection fails and nothing is sent directly. UDP to the internet is dropped (so QUIC falls back to TCP, and NTP and other UDP do not work), as is ICMP. DNS lookups still go to the platform’s resolver and are not proxied. Connections through the proxy are closed when the setting changes, and when the computer stops. One owner’s computers on a host hold at most 512 proxied connections at once together (256 through an https:// proxy), and fewer when the host is busy with other owners’; past that a new connection is closed at once. A connection whose server speaks first (SMTP, MySQL, an FTP control channel) waits about 250 ms for the computer to send first, then is tunnelled by address. A connection one side has finished is closed after 60 seconds of silence from the other side. Absent when none is set. Set at create or with PATCH /computers/{id}; a clone does not inherit it.
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.
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.
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 or null
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.
Present only on the answer to a lifecycle call — a create, a clone, or a PATCH that resized — and never on a read: the operation it started, which GET /operations/{id} answers. A clone’s is running until its disk has been copied. Absent, with the call still done, in the rare case the operation could not be recorded; a client must not treat its absence as a failure of the call.
memory_dropped
boolean
Present and true when a memory snapshot's session was asked for and the computer was built from the disk instead. Absent otherwise.
memory_dropped_reason
string
Why, when memory_dropped is set: secrets, for a snapshot of a computer that held secrets, cloned without inherit_secrets; bindings unrecorded, for one taken before its bindings were recorded, which cannot be resumed with them; capture unrecorded, for one taken before the platform recorded which capture each snapshot is, which cannot be resumed with them either.
Failures
400
Also a malformed Idempotency-Key, with code: "idempotency_key_invalid": empty, longer than 255 characters, a character outside printable ASCII or a space, or the header sent twice.
403
Also, with no reason, Verify your email before launching a computer.: the person this credential belongs to has not verified their email address. Nothing is created; verify it in the dashboard first.
409
With an Idempotency-Key, two more. code: "idempotency_in_progress", with reason: "contention", Retry-After and the operation_id: the call with this key is still being carried out — send it again shortly for its answer. code: "idempotency_outcome_unknown", with no reason: the platform did not hear how the call with this key ended (as after a 5xx answer to it that named an operation_id), so do NOT send it again blindly; read the computer, and the operation the body names, to see whether it took effect.
Also a memory snapshot whose workspace has since been deleted, cloned to resume its session (memory absent, null or true): no reason, nothing is created, and sending it again does not help. Send memory: false to build it from its disk; the new computer is in no workspace.
422
This Idempotency-Key was used within its 24 hours with a different request (code: "idempotency_key_reused"). Nothing was done; send this request under a new key.
429
Also, with inherit_secrets: true, when 16 forks of this snapshot are already waiting to be given its secrets. This one is not about your rate budget: it carries reason: "contention" and Retry-After, and nothing is created. The wait is the time until the earliest pending grant expires, rounded up to whole seconds and bounded to 1–1800 seconds. A waiting fork stops counting sooner once it has been given the secrets, once its clone is refused or built from the disk instead; otherwise its grant expires 30 minutes after it was requested. A slot can therefore open sooner, and the wait does not reserve one against another caller.
503
Also when the snapshot source cannot be confirmed; no fork is created and no grant is recorded.
Also a sealed session the platform could not open, seal or carry just then — the service that holds its key did not answer, or the host failed moving the saved session — which is the platform’s fault and not the request’s. Read the computer’s status, then send the clone again; it usually clears in a moment. Sent with an Idempotency-Key, sending it again under the same key is safe: when the host refused it before changing anything, its key was released and the answer names no operation_id, so the resend is carried out; when the answer names an operation_id, the outcome is unknown and the resend answers 409 idempotency_outcome_unknown — read the computer and that operation instead.
DELETE/snapshots/{id}
Delete a snapshotmember+
Deleting a link in an incremental chain does not lose the snapshots that depend on it — the daemon keeps what they need.
A snapshot held by two hosts at once — a move part way through handing its history over — is served from the copy on the host its computer now lives on. One that cannot be attributed that way, such as an orphan, answers 409 until the handover finishes; nothing is changed.
THE 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?include=unfinished for the id and WAIT FOR THE ROW TO GO. Its absence there is the deletion having finished; there is no state that means "deleted". A bare GET /snapshots drops the row as soon as it reads deleting, which can be long before the deletion has finished, so absence from a bare listing says only that the snapshot can no longer be restored or cloned.
A ROW THAT STAYS SAYS WHICH OF TWO THINGS HAPPENED, by its state, and it is the opposite polarity to a failed capture, which leaves no row at all. Once the daemon has detached the dependents it records the deletion and 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. A deleting row finishes without you: one held up by a backup upload of this snapshot that is slow to stop reads deleting while it waits and finishes once the upload has stopped, and one that stalls after that is retried by the daemon itself. A row still in the state it had has not recorded the deletion. It may still be working: detaching its dependents, or waiting on a slow upload while backup storage also refuses the record, which it goes on trying to write. Or it failed before recording anything (a dependent that is itself being deleted, below, or backup storage refusing the record with no upload to wait on), and then nothing was destroyed and nothing retries it. A row that stays in its old state is therefore worth a second DELETE: it is refused with a 409 while another deletion of this snapshot holds it, and otherwise accepted as one more attempt, which does no harm if an earlier one is still waiting; a 202 does not say that the first one failed.
EVERYTHING 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. Each of those 409s ends by itself and carries reason: "contention".
ONE 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.
Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
curl
base_url="${MANDALA_BASE_URL:-}"
if [ -z "$base_url" ]; then base_url='https://app.mandala.computer/api/v1'; fi
curl --fail-with-body -X DELETE "${base_url%/}"'/snapshots/snap-c1d2e3f4a5b6' \
-H "Authorization: Bearer $MANDALA_API_KEY"
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from mandala_computer import Client
with 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:
id = "snap-c1d2e3f4a5b6"
result = client.snapshots.delete(id, wait=False)
print(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
const apiKey = process.env.MANDALA_API_KEY;
if (!apiKey) throw new Error("Set MANDALA_API_KEY");
const client = new Client({apiKey, baseUrl: process.env.MANDALA_BASE_URL || "https://app.mandala.computer/api/v1"});
const id = "snap-c1d2e3f4a5b6";
const result = await client.snapshots.delete(id, {wait: false});
console.log(JSON.stringify(result, null, 2));
The deletion, accepted — the row that goes when it has finished. It omits orphaned, which this answer does not work out; GET /snapshots carries it.
Response fields
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.