---
title: "List operations"
method: GET
path: "/operations"
tags: ["Operations"]
---

# List operations

`GET /operations`

## Request

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

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.

Requires the `viewer` role or higher.

## Query parameters

- `computer_id` string
- `idempotency_key` string
- `limit` integer
- `cursor` string

## Response `200`

A page of operations, newest first.

- OperationList
  - `operations` Operation[], required
    - `id` string, required: `op_` and 24 hex characters: the `operation_id` a lifecycle call answered with.
    - `kind` 'create' | 'clone' | 'start' | 'stop' | 'suspend' | 'restart' | 'restore' | 'resize' | 'move' | 'delete', required: 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_id` string, nullable, required: 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.
    - `state` 'pending' | 'running' | 'succeeded' | 'failed', required: `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.
    - `error` object, nullable, required
      - `code` string, required: `start_failed` (a create built the computer and it would not boot), `build_failed` (a clone’s disk copy did not finish), `computer_gone` (it was deleted first), `move_failed` (nothing moved), `resize_not_applied` (the move landed and the resize did not: the computer is on another host at its OLD size), or `lost` (the platform stopped watching, or never heard how a call ended: one made with an `Idempotency-Key`, or a start, stop, suspend or restart made without one whose host did not answer in time — read the computer). More may be added.
      - `message` string, required: A sentence about it, meant for a person.
    - `idempotency_key` string, nullable, required: 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_at` string, required: 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_at` string, required: RFC 3339 timestamp of the last change of `state` or `error`.
    - `finished_at` string, nullable, required: 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.
  - `next_cursor` string, nullable, required: Pass as `cursor` for the page after this one; `null` on the last page.

## Other responses

- `400`: The request was malformed — a bad resolution string, a missing `command`, a `{pid}` or `{window}` that is not one. It is also what a well-formed request gets when the thing it names cannot be acted on and never will be — a restore onto a computer that is gone, for instance. Either way it is final: the message says what is wrong, and sending it again unchanged answers the same. That is the whole difference from a `503`, which means ask again shortly.
- `401`: Platform authentication required or refused: reason is missing, invalid, or revoked, with WWW-Authenticate: Bearer (invalid_token for invalid/revoked).
- `403`: The credential is recognized but its holder lacks the required role or membership, or the holder/account is suspended, or the holder has not verified their email address. An ordinary denial for too low a role may carry no `reason`. The email one carries none either, and its message begins `Verify your email before`: it answers a create, a clone of a computer or a snapshot, a new API key and a new SSH key, and is cleared by verifying the address in the dashboard. `reason: "revoked"` marks authority that is gone: a suspended account or a lost membership (refused as the request arrives, or if it happens while the request is in flight), or a role that was lowered while the request was in flight. The message identifies the refusal; replacing a valid key alone does not restore authority, and sending the same call again does not help.
- `405`: This public path exists but does not accept that method. Allow lists only its public methods, with HEAD for GET and metadata OPTIONS.
- `429`: You are asking faster than your plan allows. `Retry-After` says how many seconds to wait, and every metered response — anything past authentication and the re-check that your credential’s authority still holds — carries `RateLimit-Remaining`, so you do not have to be refused to find out. A `401`, a `403` from that re-check, an `OPTIONS` answer and an unexpected `500` carry none. The budget is spent in proportion to what a call costs rather than per request. What a call costs, in budget units, by kind (each operation’s exact figure is its `x-rate-cost` in the OpenAPI document): 1 to read one thing; 5 for a listing that spans the fleet, and 5 for a call into a running guest such as a screenshot or an exec; 10 for a power change and 10 to move file bytes; 25 for `GET /usage`, `GET /credits`, creating a public URL, and creating, rotating or testing a webhook; 50 to capture a snapshot; 100 to create, clone or restore a computer, and 100 to start an agent run, whose actions are then charged as they happen. The account-wide budget, as what an idle caller may spend at once and what refills each minute: No plan 1200 at once and 1200 a minute; Solo 4800 at once and 4800 a minute; Studio 12000 at once and 12000 a minute; Swarm 21600 at once and 21600 a minute. A single API key gets half of either. A `401` for a missing or unusable credential costs nothing: it is refused before anything is metered. A few refusals at the door cost one unit: a `403` because the role is too low or the route refuses an API key confined to a workspace, a `404` for a path that matches nothing, a `405`, and an `Idempotency-Key` answered from its stored result or still in progress. Everything else is charged in full when it is admitted, before it is carried out, so a refusal after that has spent the whole cost: a `400`, `402` or `409` from the route, a `403` its handler sends (on most routes, a suspended account included), and a model provider’s `401` on an agent run. The budget belongs to the account and is shared across everything on it, and any single API key may spend at most half of it. `RateLimit-Limit` is whichever of those two is binding on the request that carried it — usually the per-key half, and the account-wide figure when other keys have already spent more of it than this one has. All three `RateLimit-*` headers describe the same one, so `limit` minus `remaining` is what has been spent against it. A few `429`s are not about this budget, say so in their message, and can carry no `Retry-After`: a model provider’s rate limit relayed on an agent run. The cap on forks of one snapshot waiting for their secrets sends its own bounded expiry-based `Retry-After` with `reason: "contention"` (see `POST /snapshots/{id}/clone`).
- `500`: The application could not finish this: an unexpected exception, a build that broke, or a storage fault. Application refusals carry the request correlation ID; unexpected internal details are not returned.

---

[API](https://skmtc.dev/mandala/apis/mandala-computer-api.md) · [All operations](https://skmtc.dev/mandala/apis/mandala-computer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mandala/apis/mandala-computer-api/revisions/2afbb87fa703?raw)
