Operations

One id to wait on for any lifecycle call, whether it finished before answering or runs on after.

Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.

GET/operations

List operationsviewer+

The operations this account’s lifecycle calls started, newest first. Every accepted create, clone, start, stop, suspend, restart, snapshot restore, resize, move and delete records one and answers its id as operation_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. A call that was refused records nothing, since its error is its outcome. Operations are kept for 7 days from when they were recorded.

A call sent with an Idempotency-Key is recorded pending BEFORE it is carried out, so idempotency_key=, asked with a credential of the same scope, finds it even when its answer never reached you: it ends as its answer says, is removed if the call was refused, and ends failed with error.code: "lost" if the platform never learned how it went. A start, stop, suspend or restart sent without one whose host did not answer in time is recorded too, already failed with error.code: "lost", and its 503 names it; read the computer to see whether it took effect.

Most finish before their call answers and are recorded succeeded. A clone is running until its disk is copied, and a move until it lands. A move is brought up to date as you read it, and so is a clone, by asking its host — but only when it is among the first 5 unfinished operations on the page other than moves, newest first. A clone further down is answered as last observed; GET /operations/{id} brings it up to date.

An API key issued against a workspace sees the operations of computers in that workspace only — whoever can read a computer can read its operations, and nobody else.

The same calls made from the dashboard are recorded and listed here too, on the same terms, except a move.

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%/}"'/operations' \
  -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.operations.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.operations.list();
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "operations": [
    {
      "id": "op_89abcdef0123456789abcdef",
      "kind": "clone",
      "computer_id": "vm-b2c3d4e5f6a7",
      "state": "running",
      "error": null,
      "idempotency_key": null,
      "created_at": "2026-09-16T12:00:00.000Z",
      "updated_at": "2026-09-16T12:00:00.000Z",
      "finished_at": null
    },
    {
      "id": "op_0123456789abcdef01234567",
      "kind": "start",
      "computer_id": "vm-a1b2c3d4e5f6",
      "state": "succeeded",
      "error": null,
      "idempotency_key": "5b0f1c2e-8a4d-4f7e-9c3b-2d6a1e0f9b8c",
      "created_at": "2026-09-16T11:58:00.000Z",
      "updated_at": "2026-09-16T11:58:00.000Z",
      "finished_at": "2026-09-16T11:58:00.000Z"
    }
  ],
  "next_cursor": null
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

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, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Query parameters

computer_idstring

Only this computer’s operations. For a clone, that is the new computer. A value no computer id could be is a 400.

idempotency_keystring

Only the operation the call sent with this Idempotency-Key recorded, within the key’s 24 hours. Keys are kept per scope, so this finds the operation that a credential of your own scope reserved with the key. An operation recorded under the same key by a credential of another scope (another workspace’s, or a workspace’s when you ask with an account-wide key) is not found by it, even when the unfiltered listing shows it. A value no key could be is a 400.

limitinteger

How many to answer.

cursorstring

The next_cursor of the page before. A cursor this credential cannot read — another scope’s, one whose operation has expired, or one never given out — is a 400. An unknown parameter, or one given twice, is a 400.

Response

A page of operations, newest first.

Response fields

operationsalwaysOperation[]
next_cursoralwaysstring or null

Pass as cursor for the page after this one; null on the last page.

GET/operations/{id}

Get an operationviewer+

One operation, brought up to date: poll this until state is succeeded or failed. An id this credential cannot see is a 404, the same as one that never existed or has expired — another account’s, and for an API key confined to a workspace, one about a computer outside it. It takes no query parameters, and any is a 400.

A clone’s host is asked about its computer when this is read. If the host does not answer in time, the operation is answered as last observed rather than as an error.

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%/}"'/operations/op_89abcdef0123456789abcdef' \
  -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 = "op_89abcdef0123456789abcdef"
    result = client.operations.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 = "op_89abcdef0123456789abcdef";
const result = await client.operations.get(id);
console.log(JSON.stringify(result, null, 2));

200 Illustrative successful response.

HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17

{
  "id": "op_89abcdef0123456789abcdef",
  "kind": "clone",
  "computer_id": "vm-b2c3d4e5f6a7",
  "state": "running",
  "error": null,
  "idempotency_key": null,
  "created_at": "2026-09-16T12:00:00.000Z",
  "updated_at": "2026-09-16T12:00:00.000Z",
  "finished_at": null
}

401 Platform bearer credential missing.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer

{
  "error": "Authentication required",
  "reason": "missing",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential invalid.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "invalid API key",
  "reason": "invalid",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

401 Platform bearer credential revoked.

HTTP 401
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
WWW-Authenticate: Bearer error="invalid_token"

{
  "error": "API key revoked",
  "reason": "revoked",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

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, OPTIONS

{
  "error": "method not allowed",
  "request_id": "3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17"
}

Path parameters

idrequiredstring

The operation id.

Response

The operation.

Response fields

idalwaysstring

op_ and 24 hex characters: the operation_id a lifecycle call answered with.

kindalways"create" | "clone" | "start" | "stop" | "suspend" | "restart" | "restore" | "resize" | "move" | "delete"

The call that started it. clone is either clone; resize is a PATCH /computers/{id} that resized; move is POST /computers/{id}/move; delete is DELETE /computers/{id}, succeeded once it was accepted (its answer says what was removed and what of a purge is still queued). More kinds may be added — treat one you do not know as opaque rather than as an error.

computer_idalwaysstring or null

The computer it is about — for a create or a clone, the new one. null only for a restore whose computer could not be named when it was recorded.

statealways"pending" | "running" | "succeeded" | "failed"

pending (accepted, nothing of the computer’s has changed yet — a move still getting its target ready, or a call whose Idempotency-Key reserved it and whose answer has not arrived), running, succeeded or failed. The last two are final.

succeeded means the platform finished the step this call answers for: the guest was started, the disk was copied, the move landed at the new size. It does not mean the desktop inside has finished booting.

erroralwaysobject or null
idempotency_keyalwaysstring or null

The Idempotency-Key the call that started it was sent with, and null for a call sent without one. GET /operations?idempotency_key= finds it by this when asked with a credential of the same scope as that call’s, since keys are kept per scope.

created_atalwaysstring

RFC 3339 timestamp of when the call was answered — or, for a call sent with an Idempotency-Key, of when it was received, since it is recorded before it is carried out.

updated_atalwaysstring

RFC 3339 timestamp of the last change of state or error.

finished_atalwaysstring or null

RFC 3339 timestamp, null while it is live. For a clone, when the platform saw the copy end, which can trail the end itself by up to a minute.