Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.
GET/computers
List computersviewer+
Every computer on the account, or — when the key is scoped to one workspace — every computer in that workspace. workspace_id narrows it to one workspace, or to the computers in none. No vnc on these rows; fetch one computer to get its desktop credentials.
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%/}"'/computers' \
-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.computers.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.computers.listWithStatus();
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"
}
Query parameters
allow_partial
string
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. Rows served this way carry unreachable: true and the identity the platform keeps for the computer, and nothing its host alone knows.
state
string
Only computers in this lifecycle state — see state on Computer. Without it the listing is every computer that exists or may exist: live, unreachable and deleting. deleted and lost rows are answered from the platform’s record alone, since no host has them to list, and this parameter is the only way they are shown. An unreachable row is one no host answered for, which makes the listing short, so state=unreachable without allow_partial=1 answers either [] or a 503.
workspace_id
string
Only the computers in this workspace — an id from GET /workspaces — or, with unassigned, only the computers in no workspace. A workspace id answers exactly what a key scoped to that workspace would list, the platform’s own rows for unreachable and ended computers included. It combines with state. A workspace this key cannot reach — another account’s, one that does not exist, or any but its own for a key scoped to a workspace — is a 404, and so is unassigned from a scoped key, which cannot see those computers at all.
Response
Your computers.
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.
POST/computers
Create a computermember+
Builds a computer and, unless you pass start: false, starts it. The response carries the desktop credentials, so creating and connecting is one call. Every field has a default, so {} creates a computer from the default template; a request with no body at all is a 400.
A create that wants a running computer may be answered from the warm pool, in which case it returns in about the time the HTTP round trip takes. The pool stocks the shapes in GET /sizes, so naming a size is the likeliest way to get that answer. A cold create is slower (the guest is booted before the answer) and comes back running, or as {computer, start_error} with the computer stopped. On a host that is low on disk space, a create the warm pool would have answered is refused with 503 and nothing is created, while the pool there holds a ready computer of that shape the host has not paused, or holds only stock being held for disk-space recovery (that create answers the recovery 503); it does not fall back to a cold create then. The host confirms it built nothing, so the attempt is not counted against your plan; a host that cannot confirm it, one running older software, leaves it counted (see the paragraph on a create whose outcome was not heard, below). Otherwise — the host has paused that stock, as it pauses all of it when space is critically low, or holds none — the create is built cold and its start is refused for the same lack of space: the answer is 201 with {computer, start_error}, and that computer exists, is stopped and counts against your plan. Either way no computer starts on that host until space there has been freed, so wait before sending it again rather than retrying it in a loop.
A custom image whose verified holders have no capacity is copied on demand. During preparation, 409 with code: template_image_preparing guarantees no computer was created. Progress and errors are in preparation; repeat this request after Retry-After seconds with the returned template_transfer token to preserve the exact template and build across retries. Each retry rechecks quota and capacity. While the copy is under way it also carries reason: "contention"; a preparation that failed carries none, and preparation.error says why, unless what stopped it clears by itself (its source busy sending another image, no hypervisor yet with room for the image and the computer, or the destination already receiving it, full or not answering), when it carries contention too.
A CREATE WHOSE OUTCOME WAS NOT HEARD STILL COUNTS AGAINST YOUR PLAN. A create, or a clone, that reached a host and whose answer never arrived, or was a 5xx that cannot rule out a computer was built, is counted toward your plan’s limits from then on, with no time limit, until a listing of its host shows the computer it built. If it built nothing, it stays counted until that host is taken out of service or registered again. A host that refuses a create before building anything (a 503 from a host with no room left, for example) confirms it, and that attempt is not counted; one running older software cannot, and its 5xx is counted like any other. GET /account does not include these, so a later 402 can name more than that read shows.
The computer goes in the workspace named by workspace_id, or, without it, in the API key’s: a key scoped to a workspace creates in that workspace, and an account-wide key or a dashboard session in none.
A create’s response carries a Server-Timing header for diagnostics: name;dur=<ms> entries, the control plane’s first (cp-total, cp-lane, cp-admit, cp-place, cp-forward, and for a create bound to secrets cp-start and cp-readback), then the compute node’s (total, decode, create, copy, start, admit, prepare, qemu, load, cont, identity, with identity split into id-thaw, id-clock, id-nic, id-script, id-password, id-link and id-screen, and deliver). For a create bound to secrets the node’s start and the phases inside it come from the start that follows the create. A phase the create did not reach is left out. It is for diagnostics only: the set of names may change, so do not build logic on 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 POST "${base_url%/}"'/computers' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"name":"scratch","template":"base","resolution":"1920x1080x24","start":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:
body = json.loads("{\"name\":\"scratch\",\"template\":\"base\",\"resolution\":\"1920x1080x24\",\"start\":true}")
result = client.computers.create(**body)
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 body = {"name":"scratch","template":"base","resolution":"1920x1080x24","start":true};
const result = await client.computers.create(body);
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"
}
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
Defaults to computer. Cut to 64 characters, with control characters turned into spaces and the ends trimmed. A name left empty by that gets the default too.
workspace_id
string
The workspace to create the computer in — an id from GET /workspaces. Its secrets and an egress_proxy’s credentials_secret_id are then named from that workspace’s scope: its own secrets and the account-wide ones. Left out or null, the computer goes in the key’s workspace, or in none for an account-wide key. A workspace this key cannot reach — another account’s, one that does not exist, or any but its own for a key scoped to a workspace — is a 404, and nothing is created.
template_transfer
string
Token returned by a template_image_preparing 409. Retry the same create with this token after Retry-After seconds to preserve its template version and exact image. Progress and errors are returned in preparation. This is not a create idempotency key; stop retrying after success. A token that matches no preparation for this account and template is a 404.
size
string
A named size from GET /sizes — the fast path, since these are the shapes hosts keep pre-booted. Sets the template and the three numbers together, so it cannot be combined with template, cpu, ram_mb or disk_gb.
template
string
From GET /templates — either the short name or the pinned ref (system/base@1.2.0). Defaults to base.
The two differ in what happens when the name is wrong. A short name that is none of the templates we publish falls back to the default template — except while a host is missing from the catalogue, as X-GC-Incomplete on GET /templates shows. The name may then be one only that host has, so rather than build the default in its place the create is refused with a 503 before it is sent anywhere: nothing is done, no operation_id is named, and its Idempotency-Key is released. Send it again shortly. A size counts as the short name of its template here. A ref, a short name of one of the templates we publish that GET /templates still lists, and a create with neither template nor size are not affected by this. A ref that names nothing is refused with a 400: one that is not a well-formed ref, or a version you retired, says so and lists nothing; a version that does not exist, of a name with versions your plan can launch, lists those versions; and any other lists the templates your plan can launch. The one exception is a system/ ref while a host is missing from the catalogue: it may be one only that host has, so it is answered with a 503 instead, and is worth sending again shortly. If you would rather be told than guessed at, send the ref.
A TEMPLATE YOU PUBLISHED IS NAMED BY ITS REF AND ONLY BY ITS REF. The short form belongs to the templates we publish, so POST /templates cannot change what an existing create builds. namespace/name without a version means the newest published version, resolved once — the computer is pinned to it, and publishing again afterwards does not reach back into a machine that is already building.
cpu
integer
Cores. Defaults to the template’s. A whole number of at least 1, or the request is a 400 — except beside size, where 0 counts as left out.
ram_mb
integer
Defaults to the template’s. A whole number of at least 1, or the request is a 400 — except beside size, where 0 counts as left out.
disk_gb
integer
The template’s disk is a FLOOR, not a default: a smaller number is raised to it silently, and it is the raised figure your plan is charged for — so asking for less than the template needs can be refused 402 against a pool you did not think you were spending. A computer’s disk is an overlay over the template’s image and cannot be smaller than it. Read disk_gb on GET /templates to know the floor before you send one. A whole number of at least 1, or the request is a 400 — except beside size, where 0 counts as left out.
Send this computer’s browsers through a proxy — see browser_proxy on the computer. Linux only; any other template is refused with 400, as is a value that fails the rules there, before anything is created. A create carrying one is always a cold boot, never answered from the warm pool or a boot capture, so that the policy is on disk before the desktop starts a browser; wait for browser_proxy_pending to clear before relying on it.
A template you published can carry a default in spec.browser_proxy, and a create from it that leaves this field out gets that proxy, exactly as if it had been sent here. Send an object to use a different proxy instead, or null to create the computer with none. A create from such a template, whatever it sends here, answers 409 with reason: "unsupported" if the host it is placed on cannot launch one yet.
Send ALL of this computer’s outbound TCP through a proxy — see egress_proxy on the computer. A value that fails the rules there is refused with 400 before anything is created. A create carrying one is never answered from the warm pool, whose computers have already been on the network, and is in effect from the first packet the computer sends. Answers 409 with reason: "unsupported" if the host it is placed on cannot take one yet — or cannot take an https:// one, or one naming credentials_secret_id. A credentials_secret_id naming no secret in your scope, or one whose value is not user:password, is refused with 400 before anything is created; one whose value cannot be read on this control plane (replace its value), or named where delivering secrets is not switched on for the deployment, with 409; and where secrets are not enabled at all, with 503. Nothing is created for any of them.
resolution
string
WIDTHxHEIGHTxDEPTH or WIDTHxHEIGHT, e.g. 1920x1080x24: 640x480 to 3840x2160, both even, depth 16, 24 or 32 (24 when left out); anything else is a 400. Defaults to 1280x800x24. Create-time only — there is no route that changes it later.
secrets
object[]
Secrets to deliver into the computer at cold start, each as an environment variable or as a file. A value replaced later reaches a running computer live when it is bound as a file. Bound as an environment variable, on an image that supports it, it reaches new shells within seconds, and every exec — a plain one, which runs as root, as well as one with session: "desktop" — runs with the bound environment variables as they are at that moment, so a replaced value applies to the next exec; programs already running keep the value they started with until a restart. Named by id from the account’s secrets; a create in a workspace — the one workspace_id names, or a scoped key’s own — may bind that workspace’s secrets, and every create may bind account-wide ones. Every start delivers each secret’s latest value. At most 32; Linux only, and only on a template whose image carries the delivery helper — any other is refused with 400 before anything is created. Started — the default — it is started with its secrets delivered, and the answer is the computer as that start left it, or {computer, start_error} when the start was refused.
A computer bound to secrets suspends — by hand or when idle — and can be memory-snapshotted on any host that seals saved sessions: the image is encrypted on its way to disk with a key this API holds, and handed back only after checking the actor may still use every secret the image carries. One of those memory snapshots can be restored onto it, and it resumes holding what it held when the snapshot was taken (see POST /snapshots/{id}/restore). A DISK snapshot restored onto it starts it with its secrets delivered afresh, as a start does. Taking disk snapshots is allowed, and so is cloning — a clone copies the disk and comes out with no secrets bound, unless it is a fork of one of its memory snapshots made with inherit_secrets (see POST /snapshots/{id}/clone). It also moves between hosts: a move is cold, so no value travels. It lands stopped, and one you had running is started again for you once its secrets are delivered; one you had stopped stays stopped.
Binding secrets has to be switched on for a deployment. Where it is not, a create carrying this field is refused with 400, and secrets can still be stored and managed with /secrets or under Settings. The condition is stated rather than the answer, because this text is fixed and the setting is not. Where binding is switched on but secrets are not enabled on the control plane, so their store cannot be opened, a create naming at least one secret here is refused with 503 once its entries have been checked, and nothing is created; there an empty list binds nothing, and the create goes ahead.
Response
The computer, with its desktop credentials.
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.
Why it would not boot. The computer exists and is billable.
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.
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.
GET/computers/{id}
Get a computerviewer+
One computer, with the credentials to open its desktop.
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%/}"'/computers/vm-a1b2c3d4e5f6' \
-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"
result = client.computers.get(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 = "vm-a1b2c3d4e5f6";
const result = await client.computers.get(id);
console.log(JSON.stringify(result, null, 2));
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.
PATCH/computers/{id}
Update a computermember+
To change workspace_id, use PUT /computers/{id}/workspace. Sending it here is a 400, even alongside other fields; nothing is changed.
Rename it, resize it, change how long it may idle before its host suspends it, or set its browser proxy or its egress proxy. A request with no body is a 400. So is one naming size: a resize takes explicit cpu, ram_mb and disk_gb, which are on the row in GET /sizes.
SEND ONE OF THOSE FIVE AT A TIME. name, the sizing group (cpu/ram_mb/disk_gb), idle_suspend_min, browser_proxy and egress_proxy each refuse to travel with any of the others, and a request carrying two is a 400. They are separated because a resize needs the computer stopped and the others do not, so one request could not honour both without silently applying half of it.
A browser_proxy sent to a running computer is written into it within seconds (watch browser_proxy_pending); sent to a stopped or suspended one it is stored and applied as it starts. One sent to a host that cannot set a browser proxy yet — a null included — answers 409 with reason: "unsupported", as does a setting naming credentials_secret_id sent to a host that cannot give a browser proxy credentials yet; a host that could not be asked which it can do answers 503. Nothing is changed by any of these: send a 409 again once the host has been updated, and the 503 shortly.
An egress_proxy sent to a running computer is in effect when the answer arrives, and the connections it had open are closed; sent to a stopped or suspended one it is stored and applied before it starts. One its host cannot put into effect is refused with 503 and nothing is changed — unless the computer is being moved to another host at that moment, when the previous setting cannot be put back: then the 503 says so, the new setting is stored, and it takes effect at the host’s next successful write of its egress rules, retried every few seconds; until then the computer’s connections may go directly, or through its previous proxy if it had one. A setting sent to a host that cannot proxy a computer yet answers 409 with reason: "unsupported", as does an https:// setting, or one naming credentials_secret_id, sent to a host that cannot take those yet; a null is refused only by a host that does not know the field at all, so an egress proxy can always be removed. A credentials_secret_id naming no secret in the computer’s scope, or one whose value is not user:password, is refused with 400 and nothing is changed; so, with 409, is one whose value cannot be read on this control plane (replace its value) or one named where delivering secrets is not switched on for the deployment, and, with 503, one named where secrets are not enabled at all. A setting naming it is in effect once the host has the value, usually within seconds (watch egress_proxy_pending), and every connection is closed until then. A null, or a setting naming no credentials_secret_id or a different one, that its host answers with a 5xx, or that is sent and never answered, may or may not have taken effect, and the host is also told to forget the credentials it held for the secret the previous setting named. (A 503 whose error says nothing was changed was answered before the request was sent: it changed nothing, and the host keeps those credentials.) If the setting in effect afterwards names a credentials_secret_id — the previous one, once the host has forgotten its credentials, or the new one, whose value is not delivered after such an answer — a running computer reports egress_proxy_pending and its connections are closed until the setting is sent again, and a stopped or suspended one starts that way; if it names none, it behaves as described above.
A resize of a RUNNING computer answers 409 with reason: "running": stop it and send the same request again. Waiting does not clear it, and neither does a start.
A RESIZE DISCARDS A SAVED SESSION. Suspended counts as stopped here, so a resize of a suspended computer is accepted — and the saved desktop cannot survive it, because the vCPU count and the memory size are part of the state and growing the disk changes a device the state describes. It is dropped, and the next start is a cold boot. Resume it and finish what is open before resizing, or accept the loss deliberately.
A memory snapshot taken before a resize cannot be restored afterwards either — see POST /snapshots/{id}/restore.
Resolution is not settable — it is fixed at create.
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 PATCH "${base_url%/}"'/computers/vm-a1b2c3d4e5f6' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"name":"renamed"}'
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\":\"renamed\"}")
c = client.computers.get(id)
result = c.rename(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 = "vm-a1b2c3d4e5f6";
const body = {"name":"renamed"};
const c = await client.computers.get(id);
const result = await c.update(body);
console.log(JSON.stringify(result, null, 2));
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
On its own — not alongside a resize, idle_suspend_min, browser_proxy or egress_proxy. Cut to 64 characters, with control characters turned into spaces and the ends trimmed; a name that is empty after that is a 400 (name required).
cpu
integer
Requires the computer to be stopped. Send with the rest of the sizing group only.
ram_mb
integer
Requires the computer to be stopped.
disk_gb
integer
Grow only. Requires the computer to be stopped.
idle_suspend_min
integer or null
Minutes, and it must be the ONLY field in the request. null clears the override and returns this computer to its host’s sweep. At most 10080 (a week); a negative number or more is a 400. Zero disables idle suspend and pressure eviction, subject to the account’s never-suspend limit (Solo 0, Studio 1, Swarm 4); adding a pin beyond it returns 402. Existing pins are preserved.
On its own. Replaces this computer’s browser proxy whole; null removes it, and its files are taken out of the computer. A Windows computer is refused with 400, as is a value that fails the rules on browser_proxy.
On its own. Replaces this computer’s egress proxy whole — server and credentials_secret_id together, so a setting sent without the id removes the credentials; null removes it, and the computer’s traffic goes directly again. A value that fails the rules on egress_proxy is refused with 400.
Response
The updated 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.
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 reason: "contention" for an egress_proxy naming credentials_secret_id that reached the host after the computer moved to another workspace: nothing was changed, and sent again it is checked against the workspace the computer is in now.
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.
DELETE/computers/{id}
Delete a computermember+
Destroys the computer, keeping snapshots by default. With snapshots=delete, deletes the computer while keeping its snapshots, then deletes only the selected physical snapshot copies across the fleet. Copies with the same ID on different hosts count separately.
Read GET /computers/{id}/snapshots and pass its fingerprint as expect. A stale fingerprint returns 409 before any mutation. If expect is omitted, a fresh complete inventory defines the selected copies for this explicit purge request. Unavailable inventory or an active capture prevents purge; ordinary deletion keeping snapshots remains available.
An active capture, and a computer being moved between hosts, answer 409 with reason: "contention": send the same request again once it finishes (with expect, a capture that has finished changes the fingerprint, so read the holdings again). A stale fingerprint, a changed fleet and changed ownership carry no reason. A host that refuses the computer’s own delete passes its reason on — contention while another delete, or a copy of its disk, is already running.
Inspect computer_deleted and purge even on success: 202 means work remains queued. Partial refusals return 409; unknown transport or incomplete final inventory returns 503. A computer already deleted before this request, whose purge deleted no copy and was refused only by copies still in use (a clone reading one, say), carries reason: "contention" when it was sent with expect: send the same request again once they are free, and any copy that appeared since is refused before anything is deleted. No uncertain delete is replayed automatically. Refresh holdings before an explicit retry, including after the computer is gone. The final inventory reports new unselected copies; those need new consent. Completion covers the selected copies as observed, not a guarantee that no later import can appear.
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 DELETE "${base_url%/}"'/computers/vm-a1b2c3d4e5f6' \
-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.delete()
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.delete();
console.log(JSON.stringify(result, null, 2));
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.
Query parameters
snapshots
string
Delete this computer’s snapshots along with it. Omit to keep them.
expect
string
The fingerprint from GET /computers/{id}/snapshots. Makes the purge binding on the set you were shown. Ignored unless snapshots=delete.
Response
Computer deletion and physical-copy cleanup outcomes; 202 is pending, not completion.
Response fields
ok
boolean
Whether the requested selected-copy cleanup completed.
snapshots_deleted
integer
Physical snapshot copies confirmed deleted. Duplicate IDs on different hosts count separately. Zero when snapshots are kept.
computer_deleted
boolean or null
For fleet purge requests: true records a prior confirmed computer deletion or complete fleet absence observed during this request; it does not guarantee that the computer remains absent. false means deletion is unconfirmed or the computer was observed present again. null means the deletion outcome or current presence is unknown, including transport uncertainty or a fleet topology change.
error
string
An explanation of a partial, queued, refused, or unknown outcome.
This delete’s operation (kind delete), succeeded because the delete was accepted; this body, not the operation, says what was removed and what is still queued. 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.
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.
POST/computers/{id}/start
Start a computermember+
Boots a stopped computer, or resumes a suspended one. A cold start that would deliver secrets answers 409 while a workspace change is pending or the recorded binding scope disagrees with the computer’s current workspace. Check the workspace change in the dashboard. For conflicting bindings, remove them with PUT /computers/{id}/secrets and {"secrets":[]}, check any pending workspace change again, then retry with no bindings or bind secrets in the current workspace.
A computer the platform is holding for disk-space recovery, after a suspend or memory snapshot failed (at any free space; see POST /computers/{id}/suspend), answers 503 (“waiting for disk-space recovery”) until it resumes by itself once that recovery can finish. Stop it to discard the paused session instead; one that already reads stopped will not resume by itself, so stop it.
On a host that is low on disk space a start or resume answers 503 (“this host is low on disk space; this computer cannot start or resume until space has been freed”) and starts nothing; a suspended computer keeps its saved session. It is refused the same way until space on that host has been freed, so sending it again soon does not help. One saying the host’s disk space “cannot be checked right now” is a failed reading, and can be sent again shortly. If the message adds that its paused QEMU could not be stopped, stop the computer before trying again.
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/start' \
-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.start()
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.start();
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 computer 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.
Query parameters
resume_only
string
Resume only if a saved session still exists. A stopped computer is not booted and the call succeeds, but it is checked against your plan first like any start, so it can answer 402, or 503 when your plan usage cannot be checked.
Response
Started, or no saved session remains when resume_only=true.
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 the secret workspace refusals of a cold start, answered before anything starts: reason: "contention" while the computer is being moved to another workspace, which clears in moments, and reason: "unsupported" for a workspace change whose outcome was lost (check it in the dashboard) or recorded bindings that belong to a workspace the computer is no longer in, neither of which waiting clears (see the recovery above).
And the rest of a cold start’s secret refusals, also before anything starts. Its host holding a list of bindings that does not match the recorded one is reason: "contention" while a change to its secrets is still being saved or has just landed, and reason: "unsupported" otherwise: save the list again with PUT /computers/{id}/secrets. Such a start asks the host again before answering, and a host that cannot be asked again answers 503; send it again shortly. A bound secret deleted or out of the computer’s scope, one whose value cannot be read here or is not text an environment variable can carry, and secrets delivery not switched on for this deployment are each reason: "unsupported": the sentence names the fix, and none of them clears by waiting.
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 host that did not answer in time, whose error says the request may still have been carried out: the start may still have gone ahead. Read the computer’s status before retrying. That answer names the start’s operation_id: sent with an Idempotency-Key, that operation stays pending and resending under the same key answers idempotency_outcome_unknown instead of carrying it out again; sent without one, it is recorded failed with code: "lost".
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 start 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/computers/{id}/stop
Stop a computermember+
Powers it off. The disk is kept; you stop paying for the memory. On a suspended computer this discards the saved session, and the next start is a fresh boot.
The guest is asked to shut down and given time to do it. A graceful stop waits up to 20 seconds for the guest, then forces it off, so the call can take up to about a minute to answer. force=true skips the asking and pulls the power, which is the equivalent of holding the button in: it is what to reach for when a guest will not come down on its own, and it can lose whatever had not been written to disk.
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/stop' \
-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.stop()
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.stop();
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 computer 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.
Query parameters
force
string
Pull the power instead of asking the guest to shut down. Unwritten data is lost.
Response
Stopped.
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.
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 host that did not answer in time, whose error says the request may still have been carried out: the stop may still have gone ahead. Read the computer’s status before retrying. That answer names the stop’s operation_id: sent with an Idempotency-Key, that operation stays pending and resending under the same key answers idempotency_outcome_unknown instead of carrying it out again; sent without one, it is recorded failed with code: "lost".
POST/computers/{id}/suspend
Suspend a computermember+
Saves the running session to disk and gives the host its memory back, without ending the session. Starting it again resumes the desktop exactly where it was. The guest’s clock is set to the current time within a few seconds of the resume, through its guest agent — best effort, so a guest whose agent does not answer keeps the time it was suspended at until NTP corrects it — and the desktop panel’s clock can show the old time for up to a minute after that. A stopped computer answers 409 with reason: "unavailable": start it before suspending a session.
A computer that holds secrets suspends only on a host that seals saved sessions: its memory is encrypted on its way to disk. Elsewhere, and while its secrets are still being delivered, it answers 409: with reason: "contention" while a delivery is on its way, which passes in a moment; with reason: "unsupported" for a session its secrets were never delivered to and that nothing is delivering to now, which only a stop and a start clears; and with no reason when its guest did not say whether its secrets reached the boot it is running. For a session its secrets were never delivered to, it also answers 409 with reason: "contention" when its host could not read whether it is running just now (send it again shortly), and with reason: "unsupported" when its host cannot read its process until support repairs the host. One whose bindings dropped a secret or moved one to another revision while it ran cannot be suspended until it restarts, because its memory may still hold the old value.
A refusal known to occur before this suspend attempt starts saving includes suspend_outcome: "not_started". Without this field, a failed request may have saved a session: refresh status before further lifecycle actions. HTTP 400 or 409 alone does not establish whether saving began.
A suspend that fails, especially while the host is low on disk space, can leave the computer paused, still reading running. If start, restart, suspend or snapshot then answer 503 (“waiting for disk-space recovery”), the platform is holding it for recovery: it resumes by itself once that recovery can finish, or stop it to discard the paused session. A failure saying “this computer remains paused because the host is low on disk space” is always held that way, and so can other failures be, at any free space. A suspend sent again that is not refused that way goes ahead, and it completes a session that was saved but could not be recorded rather than saving a second one.
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/suspend' \
-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.suspend()
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.suspend();
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 computer 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
Suspended.
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.
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 host that did not answer in time, whose error says the request may still have been carried out: the suspend may still have gone ahead. Read the computer’s status before retrying. That answer names the suspend’s operation_id: sent with an Idempotency-Key, that operation stays pending and resending under the same key answers idempotency_outcome_unknown instead of carrying it out again; sent without one, it is recorded failed with code: "lost".
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 suspend 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/computers/{id}/restart
Restart a computermember+
Resets the guest — the equivalent of the reset button, not a fresh boot of a new machine. Changes that need a different QEMU command line, such as a resize, need a stop and a start instead. A stopped computer with no secrets bound is started instead, and that start can be refused like any other, including with 503 while its host is low on disk space (see POST /computers/{id}/start); a suspended one answers 409 — start it to resume, or stop it to discard the session.
A restart issues new desktop credentials and closes open desktop and terminal connections, so a vnc.url from before it stops working: read the computer again for a fresh one.
A computer that holds secrets gets them again as it comes back up, on any host that seals saved sessions. It answers 409 when it is stopped, with reason: unavailable — start it instead, which delivers them — while its secrets are still being delivered, with reason: contention, which passes in a moment, and on a host that cannot deliver them again, with reason: unsupported, where a stop and a start is the way to restart it — as it is for a session its secrets were never delivered to and that nothing is delivering to now, also reason: unsupported. For such a session, a host that could not read whether it is running just now answers reason: contention (send it again shortly), and one that cannot read its process until support repairs the host answers reason: unsupported. After a restart, secrets_delivering reads true before the computer is back up; a restart is still accepted then, for a computer that does not come back, and is refused with reason: contention only once the delivery has begun.
A computer the platform is holding for disk-space recovery, after a suspend or memory snapshot failed at any free space (see POST /computers/{id}/suspend), answers 503 (“waiting for disk-space recovery”), running or stopped, before anything is reset or any credential changes. Wait for it to resume by itself, or stop it to discard the paused session. One that is paused because its host ran out of disk space, still reading running, answers 503 too, saying so, and keeps its session: it resumes by itself once space is freed, so restart it then, or stop it. One its host paused while low on disk space is restarted, and comes back once space has been freed. A restart that finds another operation holding the computer’s monitor answers 409 with reason: contention and changes nothing; send it again.
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/restart' \
-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.restart()
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.restart();
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 computer 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
Restarting.
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.
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 host that did not answer in time, whose error says the request may still have been carried out: the restart may still have gone ahead. Read the computer’s status before retrying. That answer names the restart’s operation_id: sent with an Idempotency-Key, that operation stays pending and resending under the same key answers idempotency_outcome_unknown instead of carrying it out again; sent without one, it is recorded failed with code: "lost".
PUT/computers/{id}/workspace
Move a computer to a workspacemember+
Move to another workspace on this account, or send workspace_id: null to leave it unassigned. Requires a member or owner using an account-wide credential. Every workspace-confined API key or OAuth credential is refused with 403, including its own workspace, no-op moves and unassignment.
The body must contain exactly workspace_id: a wsp- ID followed by 12 lowercase hexadecimal characters, or null. Missing, empty, malformed or extra fields are refused; the body is capped at 1024 bytes. An unknown workspace or one in another account answers 404.
The computer must be stopped with no saved session, no snapshots on any host, no secret bindings and no proxy credentials. Nothing is automatically stopped, discarded, unbound or deleted. Snapshot history and unresolved migration copies must be verifiable across the fleet; offline history without durable proof needs operator recovery. A verified no-op returns the current public computer.
Once moved, old workspace credentials lose access and destination credentials gain access. Events and webhooks follow the new workspace; desktop and share credentials are replaced. The response contains the public Computer only, without desktop credentials or internal move state.
If the outcome is uncertain, repeating the same PUT reconciles the pending change before any further move. The refusal explains whether to retry or resolve snapshot, migration or binding state first. This route has no Idempotency-Key contract and accepts no internal request ID, proof, lease or repair fields.
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. Requires an account-wide member or owner credential and a stopped computer with no saved session, snapshots, secret bindings or proxy credentials. Send workspace_id: null explicitly for unassigned.
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 PUT "${base_url%/}"'/computers/vm-a1b2c3d4e5f6/workspace' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"workspace_id":"wsp-a1b2c3d4e5f6"}'
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("{\"workspace_id\":\"wsp-a1b2c3d4e5f6\"}")
c = client.computers.get(id)
result = c.move_to_workspace(body["workspace_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 = "vm-a1b2c3d4e5f6";
const body = {"workspace_id":"wsp-a1b2c3d4e5f6"};
const c = await client.computers.get(id);
const result = await c.moveToWorkspace(body.workspace_id);
console.log(JSON.stringify(result, null, 2));
200 The public computer in the confirmed destination workspace.
The body is not the required object containing only workspace_id. The sentence names what is wrong: a field other than workspace_id, a missing workspace_id, or one that is not a workspace ID or null.
403
Also the answer to a credential confined to a workspace, with no reason.
404
The computer or destination workspace does not belong to this account, or does not exist.
409
The computer has a saved session, is not stopped, has snapshots, bindings or proxy credentials, or conflicting move state needs recovery. A computer that is not stopped (running or suspended, say) or has a saved session is refused with reason: "unsupported": waiting does not clear it; stop it and discard any saved session first. A stopped computer whose host has not yet settled its state gets the same answer, and that one can clear by itself: if the computer is already stopped with no saved session, send the request again shortly. With reason: "contention" instead when it clears by itself: its disk is still being made, it started moving between hosts or changed while it was being checked, or its host is busy with other work on it. Send the same request again shortly. With reason: "unsupported" when a host that may hold this account’s snapshot history was taken offline without verifying it: waiting does not clear it, and an operator must recover or retire that host’s snapshot history first.
413
The body exceeds 1024 bytes.
503
The account lane is busy, a running host cannot provide verifiable snapshot history or migration copies, snapshot storage is unavailable, a snapshot transfer is pending, snapshot history or the fleet changed during the checks, or the move outcome is uncertain. Follow the refusal before retrying.
GET/computers/{id}/secrets
Read the secrets bound to a computerviewer+
Which secrets this computer is bound to, the revision of each and the environment variable or file it is published under, and the list's version, which a change sends back to say which list it is changing.
A list this platform cannot read is answered as secrets: [] with its version, though the computer is still bound. Any change to it, {"secrets":[]} included, is refused with 409 and reason: "unsupported"; contact support.
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/secrets' \
-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.secrets()
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.secrets();
console.log(JSON.stringify(result, null, 2));
Moves on every change. 0 until the list is first written; saving an empty list, or moving the computer to another workspace, writes it too.
Failures
409
The workspace change is pending, or the recorded binding scope disagrees with the computer’s current workspace; no binding list is returned. A move being made right now carries reason: "contention" and clears in moments. A change whose outcome was lost, and a disagreeing scope, carry reason: "unsupported": neither clears by waiting. Check the workspace change in the dashboard, which settles a lost one. For conflicting bindings, send {"secrets":[]} to PUT /computers/{id}/secrets without a version, then check any pending workspace change again before binding secrets in the current workspace.
PUT/computers/{id}/secrets
Change the secrets bound to a computermember+
Replaces the whole list; [] removes every binding. Each secret is resolved in the computer's own scope, as at create, and recorded at its LATEST revision — unless the entry names the revision_id it holds now, which keeps that one. Every start and restart delivers the latest value of each bound secret and moves its revision_id to it, so a replaced value needs at most a restart — one bound as a file, or as an environment variable on an image that supports it, is also sent to a running computer when it is replaced (see PUT /secrets/{id}), and moves revision_id once that lands. A binding says what was last delivered to the computer — or, until anything has been, the revision it was bound at — not what it is held to.
THE NEW VALUES ARRIVE AT THE NEXT START OR RESTART. A running computer keeps the environment it was started with until then, and a suspended one resumes holding the values it had when it was suspended. On an image that supports it, what starts AFTER the change is the exception: new shells and every exec on a running computer — plain or with session: "desktop" — see the new environment variables within seconds, with a removed one unset, while programs already running keep what they started with until the restart. A file newly bound or renamed waits for the restart either way. Restoring an older memory snapshot brings back that snapshot's values until the next reboot. A running computer whose change drops a secret or moves one to another revision cannot be suspended or memory-snapshotted until it restarts, because its memory may still hold the old value. Binding a computer for the first time needs a Linux template whose image can receive secrets, as create does, and the computer STOPPED: one started without secrets cannot receive them until it starts again, and a restart does not do that. 409 while it is running or suspended.
Send the version a read answered to change only the list you read: 409 if it has changed since. It answers 409 as well while a delivery is still in progress, and for a list that no longer binds, as a file, the secret the computer’s browser_proxy.credentials_secret_id names. It can also answer 409 during another operation, such as a stop, snapshot or suspend; retry once that operation finishes.
A nonempty list answers 409 while a workspace change is pending or the recorded binding scope disagrees with the computer’s current workspace. Before dispatch, a workspace change after binding resolution answers 409 if the computer remains visible to you; a scoped credential that loses access gets 404. An already accepted request can still answer 409 if the workspace changes while its reply is pending, even if that credential has lost access. Check the workspace change in the dashboard. For conflicting bindings, removing the entire list with {"secrets":[]} remains available wherever binding secrets is switched on (where it is not, every change to the list is a 400); omit version if reading the list answers 409. Then check any pending workspace change again before binding secrets in the current workspace.
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 PUT "${base_url%/}"'/computers/vm-a1b2c3d4e5f6/secrets' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"secrets":[{"secret_id":"csec-0123456789abcdef","env":"OPENAI_API_KEY"}],"version":1}'
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("{\"secrets\":[{\"secret_id\":\"csec-0123456789abcdef\",\"env\":\"OPENAI_API_KEY\"}],\"version\":1}")
c = client.computers.get(id)
result = c.set_secrets(body["secrets"], version=body["version"])
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 = {"secrets":[{"secret_id":"csec-0123456789abcdef","env":"OPENAI_API_KEY"}],"version":1};
const c = await client.computers.get(id);
const result = await c.setSecrets(body.secrets.map((s: {secret_id: string; env?: string; file?: string; revision_id?: string}) => ({secretId: s.secret_id, env: s.env, file: s.file, revisionId: s.revision_id})), {version: body.version});
console.log(JSON.stringify(result, null, 2));
The version a read of this list answered. Omit to change it whatever it is.
Response
The bindings as they now are.
Response fields
secretsalways
object[]
The secrets this computer is bound to.
versionalways
integer
Moves on every change. 0 until the list is first written; saving an empty list, or moving the computer to another workspace, writes it too.
Failures
409
reason says which of these clear. contention: another operation (a stop, snapshot or suspend) or a secrets delivery is in progress, another change to this list is still being saved, the computer is being moved to another workspace, its workspace changed while this change was being saved, its disk is still being copied (a create or clone still building), or its host could not read whether it is running just now; send it again once that passes. stale_revision: the list changed since the version you sent was read, or another change landed while this one was being saved; read it again and make the change against what it says now. unsupported: bindings that cannot be read here, a workspace change whose outcome was lost (check it in the dashboard), recorded bindings that belong to a workspace the computer is no longer in (remove them all with {"secrets":[]}), a computer whose process its host cannot read until support repairs the host, a running session its secrets were never delivered to that nothing is delivering to now (stop it), a first binding while the computer is running or suspended, or a list that drops the browser proxy’s credentials file; waiting changes none of them.
413
This route reads at most 16 KiB (16384 bytes) of request body, less than the general cap.
503
Also a change the computer’s host could not save: nothing was changed, and it can be sent again shortly.
POST/computers/{id}/clone
Clone a computermember+
Builds a new computer from a copy of this one’s current disk. The source is untouched, and must be stopped or suspended: a running source answers 409.
The answer arrives as soon as the new computer exists, with status: "building" while its disk is copied — poll it, or wait on its operation_id. The clone keeps the source’s size (size is a 400), lands stopped, and has no secrets bound.
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/clone' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"name":"scratch-copy"}'
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\":\"scratch-copy\"}")
c = client.computers.get(id)
result = c.clone(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 = "vm-a1b2c3d4e5f6";
const body = {"name":"scratch-copy"};
const c = await client.computers.get(id);
const result = await c.clone(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 computer 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 copy. Defaults to the source’s name followed by -clone. Cut to 64 characters, with control characters turned into spaces and the ends trimmed. A name left empty by that gets the default too.
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.
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.
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.