When a resize needs a bigger host than the one the computer is on.
Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.
POST/computers/{id}/move
Move a computer so a resize fitsmember+
Consent to moving this computer to another host in its region, so that a resize its current host cannot run becomes possible.
THIS IS THE SECOND HALF OF A REFUSED RESIZE, and it is only ever the second half. PATCH /computers/{id} answers 409 with a move object when the size you asked for is more RAM than the host this computer is on can run: {"required":true,"possible":true} means somewhere else in the region can run it, and this endpoint is how you say yes. possible:false means nothing in the region can, and there is nothing to call — change the size.
ONLY cpu, ram_mb and disk_gb ARE READ. Send the sizing group you sent the PATCH; anything else in the body is ignored rather than refused, so a rename is not carried out here — send that to the PATCH on its own once the move has finished.
It is a separate call on purpose. A resize that silently relocated your computer is exactly what this is not, so there is no flag on the PATCH that does this for you.
IT ANSWERS BEFORE IT FINISHES. 202, with the move as it stands at that moment; the disk copy runs behind it and takes as long as the disk takes. Poll GET /moves until live is false. Everything is decided again at the moment this runs — your plan, whether the computer is stopped, and which host it goes to — so a 409 here is real even though the PATCH offered the move.
The computer must be STOPPED. Suspended is not stopped for this one, unlike a resize: a saved session only loads on the host that wrote it, so it cannot travel. Resume and stop it, or discard the session, first.
ONE AT A TIME PER ACCOUNT. A second move while one is running is a 409 whichever computer it names. Naming ANOTHER computer, it carries reason: "contention" and clears once the running move settles: GET /moves says which one it is. Naming the computer already being moved, it carries no reason: that move is the answer, so follow it in GET /moves rather than sending this again.
THE COMPUTER’S SNAPSHOTS FOLLOW IT AFTERWARDS. They are carried to the new host after the move answers, and until they land, restoring one taken before the move answers 409 (see POST /snapshots/{id}/restore). If the carry fails, those snapshots stay on the old host and can still be cloned, but not restored onto this computer. A clone of one works while the carry runs, except while the new host is taking those snapshots over, when it can also answer 409; retry it once the carry has finished.
The target is ours to choose and is never in the request. You are told a host in this region, not which 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/move' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"ram_mb":32768}'
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("{\"ram_mb\":32768}")
c = client.computers.get(id)
result = c.relocate(ram_mb=body["ram_mb"])
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 = {"ram_mb":32768};
const c = await client.computers.get(id);
const result = await c.relocate({ramMb: body.ram_mb});
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
cpu
integer
Cores. The same field the resize takes; omit it to leave the count alone.
ram_mbrequired
integer
The size that did not fit. Required: it must be MORE than the computer has now — this endpoint exists for growth that needs a bigger host, and a request that would fit where it is, one leaving this out included, is a 409 rather than a move nobody needed. A body with none of the three, or no body at all, is a 400.
disk_gb
integer
Grow only, as everywhere else. The growth happens on the far side, after the copy.
Response
The move, as it stands the moment it was accepted. Read GET /moves for the rest.
Response fields
computer_id
string
The computer being moved.
state
string
Where it has got to. staging and moving and resizing are live; done, moved, failed and lost are terminal.
The three terminal failures are three different things and the difference is the whole point of them being separate words. failed means nothing happened — the computer is where it was, at the size it was. moved means the computer IS on another host but at its OLD size: the move landed and the resize did not, which is recoverable by resizing it again where it now is. lost means we stopped watching and cannot say; read the computer to find out.
detail
string
A sentence about the state, meant to be shown to a person. Empty while nothing has gone wrong.
live
boolean
It is still running. This is the flag to poll on rather than comparing state to a list.
cpu
integer
Present only when the move is applying a new value for it.
ram_mb
integer
Present only when the move is applying a new value for it.
disk_gb
integer
Present only when the move is applying a new value for it.
started_at
string
RFC 3339 timestamp.
finished_at
string
RFC 3339 timestamp. Absent while live is true.
operation_id
string
Present only on the answer to POST /computers/{id}/move: the move’s operation, which follows this row to its end. GET /operations/{id} is the same wait as polling GET /moves. 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" when another computer on this account is being moved: it clears once that move settles, and GET /moves says which. A move of this same computer already running is a 409 with no reason: follow it in GET /moves rather than sending this again. A computer whose disk is still being made carries contention too: it lands stopped by itself, and the same request then moves it. A running computer is reason: "running": stop it, and send the same request. The move’s other refusals carry no reason, and none of them clears by waiting: see above for what each wants changed.
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/moves
List movesviewer+
Every move on this account that is worth reading: the ones still running, and the ones that have finished and not yet been dismissed. This is the polling half of POST /computers/{id}/move.
A collection rather than a read of one computer, and there are two things to get from that. A move you started is found by its computer_id. And a move you did NOT start is what the "another computer on this account is being moved right now" refusal is about — only one runs per account at a time, and this is where you find out which one and how far along it is.
Finished moves stay here for a day, so an outcome is still readable by somebody who went away while it ran. Poll on live, not on the row disappearing.
An API key issued against a workspace sees the moves of computers in that workspace only.
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%/}"'/moves' \
-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.moves.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.moves.list();
console.log(JSON.stringify(result, null, 2));