Driving the desktop: what is on screen, and acting on it.
Base URL https://app.mandala.computer/api/v1. Every request carries an API key, and failures take the shape under Errors.
GET/computers/{id}/screenshot
Take a screenshotviewer+
A PNG of the desktop, in the coordinate space resolution reports. This is the image to hand a model that is about to decide where to click.
IF YOU ARE DRIVING THE DESKTOP, PASS fresh=1. A bare call may serve a frame up to 1.5 seconds old — up to 30 seconds while the computer is busy with another operation — which is fast and fine for a thumbnail and wrong for a loop: a model reading a stale frame concludes its click missed and clicks again, which is how a dialog gets dismissed twice.
A suspended computer returns its saved desktop as a JPEG up to 640 pixels wide, with X-GC-Frame: suspended, including without w. A w (or format=jpeg) is accepted but ignored there: the saved frame comes back as it was stored, at its own width. This is a stored picture, not a live screen to drive. Suspend keeps that image locally on disk until the session is resumed or discarded; it is not uploaded or included in backups. Older sessions may have no saved frame; those answer 409 with reason: unavailable, and starting the computer brings back a live one.
An explicit fresh=1 (or fresh=true) request against a suspended computer answers 409 with reason: unavailable; start it before requesting a live frame. A concurrent lifecycle operation, or another operation holding the screen with no recent frame to serve, answers 409 with reason: contention; retry after that operation finishes. A stopped computer answers 409 with reason: unavailable, as exec and input do; start it before driving its desktop.
region, scale, format and quality shape the picture: a crop, a smaller image, and a cheaper encoding, applied in that order to the same capture a bare call would return. A cropped or scaled image is in its OWN pixel space: to click on something in it, divide its position by the scale and add the region’s x and y. A suspended computer answers a crop, a scale, format=png or quality with 409 and reason: unavailable, because its saved picture is not the screen.
Every live answer names the capture it was cut from in X-GC-Capture, and gives that capture’s own size in X-GC-Capture-Size (for example 3840x2160) — the pixels region is read in, which is usually resolution but not always: a desktop resumed from a capture taken at another size, or resized from inside the guest, can differ. TO CUT A CROP FROM THE CAPTURE YOU MEASURED, send its name back as capture: the answer comes from that capture and no other, and takes no new one. Only the newest capture of a computer is held, so the next one replaces it — an ordinary call 1.5 seconds later at the earliest, a fresh=1 call at once. It is also lost when the daemon on its host restarts or the computer moves to another host. A name that is no longer held answers 409 with reason: stale_capture rather than a crop of a different picture. Take a new screenshot and use the name it carries. Measuring on a small w thumbnail and then cropping by capture costs two small requests, where measuring on the whole PNG costs the screen’s full size. A suspended computer has no capture: its saved picture carries neither header, and capture on it answers 409 with reason: unavailable.
A query parameter not listed here, or one given twice, answers 400 naming the parameters this route takes, as does a value it cannot read.
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/screenshot' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-o screen.png
Python — Mandala SDK 0.16.0
# Python 3.10+: pip install mandala-computer==0.16.0
import os
from pathlib import Path
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.screenshot()
Path("screen.png").write_bytes(result)
TypeScript — Mandala SDK 0.16.0
// Node.js 22+: npm install mandala-computer@0.16.0
import { Client } from "mandala-computer";
import { writeFile } from "node:fs/promises";
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.screenshot();
await writeFile("screen.png", result);
200 Illustrative synthetic PNG; the response contains image bytes.
Skip the cache and capture now: 1 or true. 0 or false is the same as leaving it out. Pass this whenever the screenshot is feeding a decision.
capture
string
Answer from the capture an earlier screenshot named in X-GC-Capture, rather than from the screen now, so a crop can be worked out on one answer and cut from the same pixels by the next. A capture that is no longer held answers 409 with reason: stale_capture. Not with fresh, which asks for a capture taken after the request.
w
integer
Downscale to this width and answer JPEG instead of PNG unless format says otherwise. For thumbnails. A positive whole number: 0, a negative number or anything else is a 400. A width below 64 is raised to 64, and one wider than what is being shrunk — the screen, or the region — is lowered to its width, so the picture is never enlarged. Not with scale. On a suspended computer it is accepted and ignored: the saved frame (up to 640 pixels wide) comes back unchanged.
scale
number
Downscale by this factor, greater than 0 and at most 1, keeping the encoding PNG unless format says otherwise. 0.5 halves both sides, which is a quarter of the pixels a model is sent. Not with w.
region
string
Crop to x,y,width,height, in the capture’s own pixels — the size X-GC-Capture-Size reports, which is ordinarily resolution — before any scaling. The region has to lie inside the capture: one that reaches past an edge answers 400 naming its size, rather than being clipped to a smaller picture than you asked for. With capture, it is cut from that capture.
format
string
The encoding. PNG by default, JPEG by default with w.
quality
integer
JPEG quality, 1 to 100; 72 when not given. With a PNG answer it is a 400.
Response
The current screen, or the region of it asked for, as PNG — or JPEG with w or format=jpeg. A live answer carries X-GC-Capture (the capture it was cut from, to send back as capture) and X-GC-Capture-Size (that capture’s width x height in pixels). Suspended sessions return a saved JPEG marked X-GC-Frame: suspended, with neither.
POST/computers/{id}/input
Send inputmember+
Moves the pointer, clicks, drags, scrolls, types or presses keys.
Three request shapes are accepted. The flat one below is this API’s own. The second is an Anthropic computer-use tool_use.input block posted verbatim — coordinate: [x, y], start_coordinate, scroll_direction, scroll_amount, duration, and text doing double duty as either the string to type or the modifier keys to hold — so a model’s tool call can be forwarded without translation.
The third is an OpenAI action with type instead of action: click, double_click, drag, move, scroll, keypress, type, wait or screenshot. Send one object or an array of 1–32 OpenAI action objects. Do not mix the two discriminators or dialect-specific fields. OpenAI pointer actions require both x and y, including zero. Click buttons are left, right, wheel (middle), back and forward. A drag path has 2–128 {x,y} points and holds one button through every waypoint. Coordinates are truncated to pixels, bounded to ±10,000,000, then clamped to the screen. OpenAI scroll_x/scroll_y are approximated as wheel notches: round each absolute distance divided by 100, with a minimum of one for a nonzero delta and zero for zero. Each axis is bounded to ±5000 pixels and their combined converted count to 50 notches; vertical then horizontal. Actual movement depends on the application. Optional keys (at most 16) are held around the entire OpenAI pointer gesture, including positioning, and released on failure. Arrow names, META/COMMAND and OPTION map to desktop keys. OpenAI wait lasts one second. OpenAI screenshot acknowledges a marker with ok:true; retrieve image bytes using GET /screenshot after the action(s). Duplicate or unused OpenAI fields, even zero-valued ones, are refused before auto-resume. The following alias/default rules apply to the two legacy action formats.
A batch is fully validated before auto-resume or input. Combined limits are 400 typed Unicode characters, 50 wheel notches, 128 drag points and 10 one-second waits. The whole batch shares one input lane, one QMP connection when needed, and at most 75 seconds of work including cleanup, plus two five-second queues. QMP remains held during waits in a batch containing pointer or keyboard input; wait-only batches do not hold it. VNC and native guest applications are not serialized by this API lock. Execution stops at the first error. completed and results describe finished actions; failed_index is zero-based. The failing action may have partially applied. Inspect a fresh screenshot before retrying; never automatically replay the batch. Static validation/admission failures use the normal error response. A screenshot-only batch acknowledges markers without waking the computer. Optional context is read once after a successful batch. The proxy allows 93 seconds, including context and transport.
Coordinates are absolute, in the screen space resolution reports.
Keys can be held down for the length of a click, a scroll or a drag — ctrl-click, shift-click, shift-drag to extend a selection. Give them as keys (["ctrl", "shift"]), or as a chord string in key or text ("ctrl+shift"), once. On a click or a drag they are pressed before the pointer moves and released after the button. On a scroll with a coordinate the pointer moves FIRST, with no keys held, and the keys are then pressed for the wheel notches and released after them. Either way they are released when the action fails part way.
A body is read strictly. A field this endpoint does not have, a field the action does not read (keys on a move, a coordinate on a type, button on a click), the held keys given in more than one field, and an unknown action each answer 400 naming what was wrong, before anything is sent to the computer and before a suspended one is resumed. A field at its zero value — "", [], 0 — counts as not sent, except x and y, where 0 is a position: {"action":"key","key":"ctrl+c","x":0,"y":0} is a 400 (key does not take x). Leave them out, or send null.
amount (or scroll_amount) is 1 to 50 notches, 3 when left out or not above 0; more than 50 is a 400. duration is at most 30 seconds, 1 second when left out or not above 0; longer is a 400. Those two, a scroll direction other than up, down, left or right, and a key name this endpoint does not know — in a chord or held during a pointer action — are checked with the rest of the body: a 400 before a suspended computer is resumed, and the same 400 from a stopped one.
type requires non-empty valid Unicode text, at most 400 characters. Plain ASCII is typed as physical key events, about 12 ms a character (each key held 8 ms, then 4 ms before the next). Text containing any other character is typed WHOLE, in request order, by one guest helper: its ASCII at the same physical key positions and shift levels, and every other character by GTK Unicode composition. This works in supported Chromium GTK3 and Xfce Terminal/VTE configurations on Linux X11, including accents, CJK, emoji and combining sequences, without changing the clipboard or keyboard mapping. Tab and LF are keys; CRLF sends one Return. Bare CR and other C0/C1/DEL controls are refused. Malformed or unsupported text is checked before auto-resume or any keys. Unicode typing requires XTEST, XRes process identification, Python Xlib, GTK3, timeout, a neutral supported US keyboard mapping and an effective gtk-im-context-simple input context. A no-key preflight verifies the supported application/configuration before even a mixed request’s ASCII prefix. Firefox, other applications, GTK4, custom input modules, disabled GTK IME, Windows and native Wayland Unicode typing are unsupported. Guest configuration is not changed. The response reports mechanism: physical | unicode | mixed. Use exclusive desktop control and verify the resulting text: delivery does not prove application acceptance. Runtime failure or cancellation can leave partial text or composition state. Cancellation waits for the bounded in-flight helper; cleanup never sends Escape into uncertain focus. Unicode requests share 75 seconds for all work including cleanup, with a 95-second proxy allowance for queues and transport; inspect before retrying and never automatically replay.
For fast text insertion use {"action":"paste","text":"Café — 東京 😀"}. This writes the Linux desktop clipboard and sends Ctrl+V. Use key: "ctrl+shift+v" for terminals that require it, Shift+Insert is refused because it can read the separate primary selection. Text must be non-empty UTF-8 without NUL, at most 8192 bytes. Windows and guests without working clipboard support are refused.
Paste replaces the clipboard and leaves it in place. API input/clipboard writes are serialized through the shortcut, but native guest applications and VNC clients can still change it. Use exclusive desktop control and wait for the target to consume the paste before changing the clipboard again. A 200 means the clipboard write and shortcut were delivered, not that the application accepted or inserted the text. Check the target afterward; paste-disabled fields may ignore it. An interrupted request can have side effects: inspect before retrying, and do not automatically fall back to typing or replay it.
count repeats a left_click, right_click or middle_click up to 10 times, for the applications that read a click count past three.
Add ?context=1 to have the answer carry the desktop’s windows and the focused one as they stand just after the action — on any action, wait included. A query parameter other than context, one given twice or empty, and a context other than 1, true, 0 or false each answer 400 before anything is sent to the computer.
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/input' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"action":"left_click","x":640,"y":400}'
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("{\"action\":\"left_click\",\"x\":640,\"y\":400}")
c = client.computers.get(id)
result = c.click(body["x"], body["y"])
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 = {"action":"left_click","x":640,"y":400};
const c = await client.computers.get(id);
const result = await c.click(body.x, body.y);
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.
Query parameters
context
string
Answer the desktop’s windows after the action — any action, wait included — as context, saving the round trip to GET /computers/{id}/windows, and, when the focused window is Chromium, the page on screen as context.dom. Costs one windows read and at most one page read, bounded at 3 seconds together, after the action. If the windows cannot be read the action still answers ok: true, with context_error saying why and no context; if only the page cannot (any other window focused included), context has no dom and context_error says why. 1 or true asks; 0, false or leaving it out does not.
Request body
action
string
One of move/mouse_move, left_click, right_click, middle_click, double_click, triple_click, left_mouse_down, left_mouse_up, left_click_drag, scroll, type, paste, key, hold_key, cursor_position, wait.
x
integer
Absolute. Omit to act where the pointer already is, except on move/mouse_move and left_click_drag (whose destination it is): those need a coordinate, and one sent without it is refused with a 400 before a suspended computer is woken. 0 is a position, not an omission — except on scroll, where a flat x: 0, y: 0 scrolls where the pointer is, and coordinate: [0, 0] is needed to scroll at the top-left corner.
y
integer
coordinate
number[]
[x, y], as an alternative to x/y.
start_coordinate
number[]
Where a left_click_drag starts.
text
string
The string for type (non-empty, at most 400 Unicode characters) or paste (1–8192 UTF-8 bytes, no NUL). For key and hold_key, the chord. On a click, a scroll or a drag, the keys to hold down during it.
key
string
A chord such as ctrl+c, for key and hold_key. On a click, a scroll or a drag, the keys to hold down during it. For paste: ctrl+v (default) or ctrl+shift+v.
keys
string[]
The chord as separate keys, for key, hold_key and paste. On a click, a scroll or a drag, the keys to hold down during it.
button
string
Scroll direction: up, down, left, right.
scroll_direction
string
The computer-use spelling of button.
amount
integer
Scroll notches, 1 to 50; 3 when left out. More than 50 is a 400.
scroll_amount
integer
The computer-use spelling of amount.
duration
number
Seconds, for wait and hold_key: at most 30, and 1 when left out. Longer is a 400.
count
integer
How many times left_click, right_click or middle_click presses its button, 1 to 10; 1 when left out. The presses are paced as double_click’s are, so the guest sees one multi-click. More than 10, or below 1, is a 400, and so is count on double_click or triple_click, which are two and three clicks by name.
OpenAI action discriminator; mutually exclusive with action.
path
object[]
Required for drag: ordered {x,y} waypoints.
scroll_x
number
Required for scroll: horizontal pixel delta, ±5000.
scroll_y
number
Required for scroll: vertical pixel delta, ±5000.
Response
Delivered. type reports its mechanism; cursor_position answers where the pointer is and whether that is known. Verify application acceptance.
Response fields
okalways
boolean
The action was carried out.
mechanism
"physical" | "unicode" | "mixed"
Present on successful type: physical for plain ASCII typed as US-layout key events; unicode for text with no ASCII, typed by GTK Unicode composition; mixed for both, typed in request order by the same guest helper, which pressed the ASCII key positions itself. Confirms dispatch; verify that the target accepted the exact text.
x
integer
Where the pointer is, for cursor_position. Absent on every other action.
y
integer
The same.
known
boolean
Read this BEFORE x and y. QEMU’s tablet is an absolute pointing device: it accepts coordinates and reports none back, so the only position this platform can tell you about is one it put the pointer at itself. false means nothing has — a guest that has just booted, or been restored, has its pointer wherever it left it — and x and y are then 0, which is a corner the pointer is probably not in rather than a reading. Absent on every action but cursor_position.
The desktop just after the action, when the request asked with ?context=1 and its windows could be read. Absent otherwise.
context_error
string
What ?context=1 could not read. With no context, it says why the windows could not be listed: the sentence GET /computers/{id}/windows would have answered (a Windows guest, no desktop session, a guest agent not answering), or that the guest did not list its windows within 3 seconds. Beside a context, it says why that context has no dom: no window has focus, the focused window is not Chromium, Chromium is not listening on its debugging port, the focused Chromium is not the desktop’s own profile or no tab of it can be tied to the focused window, its page does not fill its window (a side panel or DevTools docked beside it), the computer’s image predates page elements, or the page was not described within the same 3 seconds. Either way the action itself was carried out — ok is still true — so do not send it again; list the windows or take a screenshot instead.
completed
integer
Number of completed actions, in order. Not a rollback guarantee.
Zero-based index of the failed action, only on execution failure. That action may have partially applied.
error
string
Execution failure; inspect the desktop before retrying.
Failures
503
Also, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.
POST/computers/{id}/exec
Run a commandmember+
Runs a command inside the guest and waits for it, up to timeout_s.
On the hosted API a foreground request is cut off at the edge after about 120 seconds, whatever timeout_s says (a 524). The command keeps running, and until timeout_s runs out — or the command ends, if that is sooner — it holds the computer’s guest agent, so another exec, a clipboard, window or file call on that computer can answer 409 in that time, and so can input that goes through the guest agent: a paste, or a type whose text is not plain ASCII. Screenshots, pointer and key input, and a type of plain ASCII text do not wait on it. Use background: true for anything that may take more than about 100 seconds.
Pass background: true for a command that outlives the request: the response is a handle carrying the guest pid, and you read its output with GET /computers/{id}/exec/{pid} and stop it with the DELETE. timeout_s means nothing alongside it, because not waiting is the whole request. One computer may hold at most 16 background commands. A seventeenth answers 409 without a reason: the existing commands may be long-lived servers, so retrying does not necessarily help; stop one of the handles you already hold first.
A stopped computer answers 409 with reason: "unavailable", a foreground command or a background one: start it first. Waiting does not clear it. A computer that stops while its command is starting or running answers the same, and that command may have run in part, so decide for yourself whether to send it again. A computer being moved to another host answers 409 with reason: "contention" instead, and the same request works once the move finishes. A command the move itself cuts off answers 409 unavailable, as any other stop does: it may have run in part, so it is not one to send again without deciding to.
A suspended computer is resumed to run the command — after session, env and cwd have been checked, so a request refused for one of them, or for asking a Windows guest for a desktop or background command, never resumes or bills the computer.
Explicit retain_output:true or a finite options object captures only the already-returned completed synchronous response. Missing/false performs no retained storage work. The option is public/internal only; it does not open session exec. Effective background:true with retention, duplicate canonical options, or trailing non-whitespace after an enabled first object is invalid_retain_output 400 before dispatch. Original request bytes are forwarded unchanged. Optional admission adds at most 1000 milliseconds and publication at most 5000 milliseconds; inspection is bounded at 50331648 response bytes. Only successful immutable publication adds result_id. Every optional failure preserves the original command status/body with X-GC-Retained-Output: unavailable. Missing/false adds no header. Original credential loss takes precedence over private delivery; a refused or lost response does not prove the command did not run. Never replay exec to repair retention.
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/exec' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"command":"ls -la /home/user","timeout_s":30}'
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("{\"command\":\"ls -la /home/user\",\"timeout_s\":30}")
c = client.computers.get(id)
result = c.exec(body["command"], timeout=body["timeout_s"])
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 = {"command":"ls -la /home/user","timeout_s":30};
const c = await client.computers.get(id);
const result = await c.exec(body.command, {timeoutS: body.timeout_s});
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.
Request body
commandrequired
string
The command line.
session
string
Empty runs as the guest agent’s own account — root, or SYSTEM on Windows. desktop runs inside the console user’s graphical session, which is where a GUI application has to start: launch one any other way and it has no display to appear on.
desktop is refused outright on Windows guests — there is no way to reach the interactive session there yet — and the refusal carries reason: "unsupported", the word for something no action on the computer changes. It is answered before the computer is asked whether it is running, so a stopped Windows computer tells you this rather than telling you to start it first. Empty session is unaffected: an ordinary command runs on Windows through cmd.exe.
On Linux, a computer made from an older image has a shell profile hook that would run its bound secrets as code, and the platform replaces it before the first desktop command. If that replacement fails (a full or read-only guest disk, say), desktop answers 503 with no reason and nothing is run; the next request tries the replacement again. An empty session runs as root, which that hook skips, and is never refused for it.
timeout_s
integer
Seconds to wait. Defaults to 30, and 600 is the most it accepts — the wait holds the computer’s guest agent, so no other call that needs it reaches that machine for its length, and a higher value is a 400 rather than a quietly shorter wait. Send background: true for anything longer. Ignored when background is set. On the hosted API the edge cuts a foreground request off after about 120 seconds whatever this says. The command keeps running and holds the guest agent until this runs out, or until it ends if that is sooner, so exec, clipboard, window and file calls on that computer can answer 409 until then, as can a paste input or a type whose text is not plain ASCII: use background: true for anything that may take more than about 100 seconds.
background
boolean
Answer with a handle instead of a result. Refused on Windows guests with 400 and reason: "unsupported", before the computer is resumed.
Working directory inside the guest: an absolute path, or the request is a 400. If the shell cannot enter it, no part of the command runs and exit_code is 127, on Linux and on Windows alike. On Windows a UNC path, or one containing ", % or !, is also a 400, and the command runs under cmd.exe as (cd /d "<cwd>" || exit 127) & <command>, so a compound command is parsed as it would be on its own. cmd.exe expands %VAR% across that whole line before any of it runs, so a %CD% in the command reads the directory cmd.exe started in, not cwd. Whether a Windows guest enters cwd this way has not yet been measured: if it does not, every such command exits 127 without running. A 127 alone does not prove that, since a command that ran can exit 127 too.
env
object
Environment variables. At most 64 entries, each at most 4096 bytes as NAME=value — the name, the value and the = between them count together, so a long value leaves less room for a long name. Past either is a 400, and so are an empty name, an = inside a name, and a NUL in either half — the last two because the guest agent would carry them silently rather than refuse. The per-entry refusals name the entry they mean; going over the COUNT reports the two counts, since there is no one entry at fault. Nothing partial runs. These bound what the guest agent is asked to spawn, so they apply whether or not background is set.
What these are added TO is worth saying, because it is not what the guest agent was started with. On Linux your command runs through a login shell, so the base is whatever the guest user’s profile sets — PATH included — and your entries go on top of that. A variable the guest agent happened to hold and the profile does not set is not there to inherit. On Windows a command runs through cmd.exe, which sources no profile, so there is no such base; background is refused there outright.
Secrets bound to the computer as environment variables are in that base too, on an image that supports it, for every command — a plain one run as root as well as one with session: "desktop" — at their values when the command starts, so a replaced value applies to the next command. Windows computers cannot be bound to secrets.
An entry here wins over both: over a bound value of the same name, and over whatever a login profile sets while the shell starts (a template’s defaults, PATH), because your entries are applied again, as plain strings, after the profiles and the bound values, just before your command. Not applied again: a name a shell cannot assign or that it uses itself (such as PS4, or anything beginning BASH_), which keeps what the shell made of it; a variable a profile made read-only; and, with session: "desktop", the session’s own DISPLAY, XAUTHORITY, DBUS_SESSION_BUS_ADDRESS, XDG_RUNTIME_DIR, WAYLAND_DISPLAY, HYPRLAND_INSTANCE_SIGNATURE, HOME, USER and LOGNAME, which stay the session’s: a session that has one keeps its value, and one it does not have is set only by your entry, if you give one, and otherwise unset.
Your entries are ALSO in the environment the login profiles run under, from the moment the shell starts: they are applied again after the profiles, not instead of being there during them. So overriding PATH, or anything else a profile reads, changes what the profiles do, and can break one; a profile that fails that way adds its errors to the command’s stderr.
Response
The result, or a handle when background was set.
Response fields
result_id
string
Optional committed synchronous retained version, present only after explicit retain_output capture succeeds. Never an execution ID or replay key.
exit_code
integer
stdout_b64always
string
What the command printed on stdout, base64. Decode it: echo "$STDOUT_B64" | base64 -d, or your language’s own decoder.
Base64 rather than text because a JSON string is UTF-8 by definition and a command’s output is bytes. A field carrying the bytes directly could only carry the ones that happen to be valid UTF-8, and everything else — a tarball, a PNG, a latin-1 build log — arrived silently rewritten, byte by byte, into U+FFFD. There is no flag for that and there could not be a useful one: half a response would be intact and half replaced, with nothing to say which.
stderr_b64always
string
The same, for stderr.
timed_out
boolean
The command hit timeout_s and was not waited for any longer. Check this BEFORE reading exit_code — a timed-out command has not reported one, and exit_code then reads -1, a sentinel and not the command’s status.
out_truncated
boolean
The guest agent stopped capturing stdout before the command stopped producing it, at 16 MiB. What you have is a prefix, and nothing else in the response would tell you so.
err_truncated
boolean
The same, for stderr.
execution_id
string
Opaque stable identity for the execution routes. Never use a PID to identify stored work.
pid
integer
The guest pid. This is the {pid} path segment on the two follow-up routes.
command
string
The command line, echoed back.
running
boolean
exited
boolean
False on a handle. The command has just been started; this is ExecStatus’s question.
stdout_offset
integer
0, the point the first read starts from.
stderr_offset
integer
0.
started_at
string
RFC 3339 timestamp.
Failures
503
Also, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.
POST/computers/{id}/browser-connections
Open a browser connectionmember+
Launch or reuse a dedicated Chromium profile in the desktop session of a running Linux computer. Send {} for the legacy ten-minute grant, or lifecycle_version: 2 for a renewable session. Requires the guest streaming channel and Chromium. Returns a CDP WebSocket capability, with at most eight grants per computer and one active socket per grant. Use the returned URL with an Authorization: Bearer header containing the returned token, spelled exactly Authorization: Bearer <token>: Bearer capitalised, one space, and nothing after the token. Other spellings the REST API accepts are a 401 here. Account API keys, desktop tokens and query-string tokens do not authenticate this socket. Legacy expiry closes the socket and permits reconnect before expiry. Version 2 keeps expires_at as an immutable alias of attach_expires_at: attach once, before the shorter of ten minutes and lease_seconds. Its active lease defaults to 1800 seconds and the absolute session cap is 7200 seconds from creation. Renew the existing attached connection with POST /computers/{id}/browser-connections/{connection}/renew using account authorization. No independent idle timeout applies. Version 2 disconnect revokes the grant; renewal never extends the attachment credential or reconnects Chromium. DELETE closes either version without stopping the browser. The managed profile persists on disk and is shared by grants for this computer; it is separate from the default desktop profile. A stop, restart, suspend, migration, desktop-credential rotation or daemon restart invalidates the grants. Issued capabilities are delegated access: revoking the issuing API key does not itself revoke them; delete the grant or wait for expiry. Raw CDP grants full control of that browser, including JavaScript and guest file access; it is not a read-only or tab-scoped capability. WebSocket reconnect never replays commands. Clients must decide whether an interrupted action completed. The endpoint is Chromium CDP, not the Playwright server protocol.
The WebSocket handshake answers 101 when it attaches. It answers 400 when a GET whose token the host accepts is not a WebSocket upgrade; 401 for any other method, a missing, expired or revoked token, a query string on the URL, a version 2 grant that has already attached once, or a computer or account that is no longer active: a computer deleted, being deleted or written off with its host, or a suspended account. A computer whose host is only unreachable is still active; see the 503 below for what it gets. 409 when a legacy grant already has an open socket, or another handshake with the same token is still under way; 502 when the browser in the guest cannot be reached, so request a new connection; and 503 when the computer’s host cannot be found or does not answer and the host accepted this token on a handshake within the last ten minutes. That 503 says only that the token cannot be checked now. Once the host answers, a legacy grant that has not expired, or a version 2 grant that never attached, is answered on its merits; a version 2 token that has attached is spent and gets the 401, so request a new connection. A token the host has not accepted within the last ten minutes cannot be checked while the host is unreachable and gets the 401; if you just created it, read the computer or request a new connection, which say whether its host is reachable. Under heavy load, or after many refused attempts from your address within a minute, it can also answer 503 with Retry-After instead of judging the token: send the same token again after that many seconds and it is answered on its merits, 101 if it is good and 401 if not. A 401 is never about load.
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/browser-connections' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{}'
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.create_browser_connection()
print("Token returned once; store result.token securely without logging it.")
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.createBrowserConnection();
console.log("Token returned once; store result.token securely without logging it.");
201 A short-lived browser capability. The token is illustrative.
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.
Request body
lifecycle_version
integer
Opt in to renewable leases. Omit all options for legacy behavior.
lease_seconds
integer
Requires lifecycle_version: 2. Must not exceed max_duration_seconds. The default is not lowered to fit: with a max_duration_seconds below 1800, send lease_seconds too, or the request is a 400.
max_duration_seconds
integer
Requires lifecycle_version: 2. Absolute lifetime from creation; renewal cannot extend it.
Response
A browser connection and its expiry.
Response fields
idalways
string
urlalways
string
The CDP WebSocket URL. Supply token in the Authorization: Bearer header.
tokenalways
string
A delegated browser capability. Keep it out of URLs, logs and model messages.
expires_atalways
string
Legacy: attachment and socket expiry. Version 2: immutable alias of attach_expires_at; the active lease is separate.
lifecycle_version
integer
server_time
string
attach_expires_at
string
lease_expires_at
string
absolute_expires_at
string
lease_seconds
integer
idle_timeout_seconds
integer
No independent inactivity timeout; lease and absolute deadlines still apply.
Failures
409
reason says whether waiting helps. unsupported: a Windows computer, or a running or suspended one whose hardware lacks the guest streaming channel because it was last started before interactive terminals existed (a stop and a start adds it), or one whose process its host cannot read until support repairs the host; sending the same request again never helps. Also unsupported when the computer already holds eight grants: revoke a connection with DELETE before opening another. A grant can otherwise hold its slot until its expiry, up to two hours. contention: the computer changed while the browser was opening, its host could not read whether it is running just now, its guest agent is busy with another operation (a snapshot’s capture among them) or another terminal connection is being set up on it, or a suspend has begun; send it again — after a suspend, the answer is the unavailable below. starting: the guest agent has not answered yet inside its boot window, as while the computer boots or restarts; send it again in a few seconds. unavailable, with resume_required: true when it is suspended: the computer is not running, or stopped while the browser was opening, so start it — a stopped one without the channel gets it from that cold start. No reason when the guest has no active desktop session, as on the other desktop routes: a desktop still starting looks the same as one nobody is signed in to. The WebSocket handshake’s own 409 carries contention. On a legacy grant it means another socket is open or opening, and it clears when that socket closes, if the grant has not expired by then. On a version 2 grant it means another handshake with the same token is still under way: if that one attaches, the token is spent and gets 401; if it does not, the token can attach again before attach_expires_at.
413
The options body is over 1024 bytes.
502
The browser could not be opened: the launcher in the guest failed or answered with no usable endpoint, the browser transport could not be prepared, or the guest agent is still not answering past its boot window. No reason, because none of these clears by waiting; a guest that is still booting, busy or being suspended is the 409 above instead.
503
Also the answer to lifecycle_version: 2 on a computer whose host does not support renewable sessions yet (Renewable browser sessions are not available on this computer yet.). No token is returned and the grant the host opened is revoked. If it cannot be revoked, the message says so: that grant holds one of the computer’s eight connections until it expires, within ten minutes, and nobody can use it. Send {} for a legacy grant, or try again later.
Immediately close the socket and revoke this computer’s named browser capability. Idempotent for an absent grant. Does not close Chromium or erase its managed profile.
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/browser-connections/a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4' \
-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:
connection = "a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
id = "vm-a1b2c3d4e5f6"
c = client.computers.get(id)
result = c.revoke_browser_connection(connection)
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 connection = "a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4";
const id = "vm-a1b2c3d4e5f6";
const c = await client.computers.get(id);
const result = await c.revokeBrowserConnection(connection);
console.log(JSON.stringify(result, null, 2));
Set a version 2 session’s lease_expires_at to the earlier of now plus its lease_seconds and the original absolute_expires_at; renewal never shortens it. Requires fresh member-level account authorization and the same computer generation. The browser capability cannot authorize renewal. Expired, revoked, legacy, unattached or disconnected sessions return 409; they are never recreated. At the absolute cap, a still-active lease can return the unchanged deadline. No browser action is replayed. Revoking an account API key or membership prevents renewal; existing delegated access remains valid only until its current lease ends or is explicitly revoked.
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/browser-connections/a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4/renew' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{}'
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:
connection = "a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
id = "vm-a1b2c3d4e5f6"
c = client.computers.get(id)
result = c.renew_browser_connection(connection)
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 connection = "a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4";
const id = "vm-a1b2c3d4e5f6";
const c = await client.computers.get(id);
const result = await c.renewBrowserConnection(connection);
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.
connectionrequired
string
The connection id from POST /computers/{id}/browser-connections.
Response
Updated deadlines for the same connection; no replacement token.
Response fields
idalways
string
lifecycle_versionalways
integer
server_timealways
string
attach_expires_atalways
string
lease_expires_atalways
string
absolute_expires_atalways
string
lease_secondsalways
integer
idle_timeout_secondsalways
integer
Failures
409
reason: "contention" while this connection’s WebSocket handshake is still in progress: renew it once the socket is open. Otherwise reason: "unsupported": the session is not an active renewable one. It expired, was revoked or disconnected, was invalidated by a stop, restart, suspend, migration or desktop-credential rotation, is a legacy grant, or has not attached. Waiting never changes that answer and the session is never recreated: open a new connection, or attach this one first if it has not.
413
The body is over 1024 bytes.
GET/computers/{id}/exec/{pid}
Read a background commandmember+
What a backgrounded command has printed since you last asked, and whether it is still running.
The read is CONSUMING and the cursor is the daemon’s, not yours: each call returns what has arrived since the previous call and advances the handle’s offset itself. There is nothing to pass — the stdout_offset and stderr_offset on the response are how far it has now read, reported so you can tell how much has gone by, not a parameter to send back. Output you receive and drop is gone.
stdout_b64 and stderr_b64 are base64 and always are; the offsets count the DECODED bytes, which is what makes them line up across polls.
more is the field to poll on: it says there is further output waiting right now.
A handle answers 404 from ten minutes after its command exits, and after the daemon on the computer’s host restarts — the same 404 as a pid that never existed. A stopped or suspended computer answers 409 with reason: "unavailable", and this route does not resume it. A suspended one still holds the command: after a resume, its output and exit code can be read again.
Only commands this API started can be read this way.
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. PID output reads consume incremental output; use execution-ID offset reads for stable retrieval.
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/exec/4242' \
-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"
pid = "4242"
c = client.computers.get(id)
result = c.background_command(int(pid)).poll()
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 pid = "4242";
const c = await client.computers.get(id);
const result = await c.execPoll(Number(pid));
console.log(JSON.stringify(result, null, 2));
Its state and whatever it has printed since the last read.
Response fields
execution_idalways
string
The stable ID originally returned by background exec.
pidalways
integer
commandalways
string
The command line, echoed back.
runningalways
boolean
exitedalways
boolean
exit_code
integer
Absent until it has exited — absent rather than 0, which is the one value worth trusting.
stdout_b64always
string
What it has printed since your previous read, base64. This read consumes it.
Always base64, on every response, for the reason Exec gives — and here there is a second one. A poll stops at a 1 MiB ceiling, and that cut lands on a byte offset: a multi-byte character sitting across it would have had BOTH of its halves replaced. Ordinary text output was being corrupted at every megabyte, not only binary output. Decoded, the two halves join back into the character they came from.
stderr_b64always
string
stdout_offsetalways
integer
How far the daemon has now read. Reported, not a parameter — see the note above.
stderr_offsetalways
integer
more
boolean
There is further output waiting right now. This is the flag to poll on.
ABSENT rather than false when there is not, so false is a value this field never carries: code waiting for more === false waits for ever. Polling is safe either way — !more reads the same for absent and false — and no schema here marks it required, so an ordinary client simply sees undefined. What it is not safe to do is declare it a required boolean and put a strict runtime validator behind that, which rejects the ordinary response rather than the unusual one.
killed
boolean
A DELETE /computers/{id}/exec/{pid} reached this command while the platform still had it running. Absent rather than false otherwise, on the same terms as more.
That is weaker than “it did not finish on its own”, and the difference is a real one rather than a caution. Whether a command has exited is noticed by a poll, so a command that ended a moment before your DELETE arrived is still running as far as this platform knows: the stop is carried out against a process group that has already gone, which succeeds, and the flag is set. So killed tells you what YOU did, not how the command ended. exit_code is what says that, and a command stopped by a signal reports the code its shell gives for one.
started_atalways
string
RFC 3339 timestamp.
DELETE/computers/{id}/exec/{pid}
Stop a background commandmember+
Stops a backgrounded command and answers the handle as it was when it stopped, with whatever it had printed and not yet been read.
The signal goes to the PROCESS GROUP, so a command that started children takes them with it: TERM first, then KILL if it is still there when the grace period ends. killed on the answer means this call did the stopping — including when the command had in fact already ended and nothing had noticed yet, so do not read it as "your command was cut short". See the field.
A handle answers 404 from ten minutes after its command exits, and after the daemon on the computer’s host restarts — the same 404 as a pid that never existed. A stopped or suspended computer answers 409 with reason: "unavailable", and this route does not resume it. A suspended one still holds the command: after a resume, its output and exit code can be read again.
Only commands this API started can be stopped this way, and a {pid} that is not a number is a 400 rather than a 404.
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. PID output reads consume incremental output; use execution-ID offset reads for stable retrieval.
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/exec/4242' \
-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"
pid = "4242"
c = client.computers.get(id)
result = c.background_command(int(pid)).kill()
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 pid = "4242";
const c = await client.computers.get(id);
const result = await c.execKill(Number(pid));
console.log(JSON.stringify(result, null, 2));
The handle as it was when the command was stopped.
Response fields
execution_idalways
string
The stable ID originally returned by background exec.
pidalways
integer
commandalways
string
The command line, echoed back.
runningalways
boolean
exitedalways
boolean
exit_code
integer
Absent until it has exited — absent rather than 0, which is the one value worth trusting.
stdout_b64always
string
What it has printed since your previous read, base64. This read consumes it.
Always base64, on every response, for the reason Exec gives — and here there is a second one. A poll stops at a 1 MiB ceiling, and that cut lands on a byte offset: a multi-byte character sitting across it would have had BOTH of its halves replaced. Ordinary text output was being corrupted at every megabyte, not only binary output. Decoded, the two halves join back into the character they came from.
stderr_b64always
string
stdout_offsetalways
integer
How far the daemon has now read. Reported, not a parameter — see the note above.
stderr_offsetalways
integer
more
boolean
There is further output waiting right now. This is the flag to poll on.
ABSENT rather than false when there is not, so false is a value this field never carries: code waiting for more === false waits for ever. Polling is safe either way — !more reads the same for absent and false — and no schema here marks it required, so an ordinary client simply sees undefined. What it is not safe to do is declare it a required boolean and put a strict runtime validator behind that, which rejects the ordinary response rather than the unusual one.
killed
boolean
A DELETE /computers/{id}/exec/{pid} reached this command while the platform still had it running. Absent rather than false otherwise, on the same terms as more.
That is weaker than “it did not finish on its own”, and the difference is a real one rather than a caution. Whether a command has exited is noticed by a poll, so a command that ended a moment before your DELETE arrived is still running as far as this platform knows: the stop is carried out against a process group that has already gone, which succeeds, and the flag is set. So killed tells you what YOU did, not how the command ended. exit_code is what says that, and a command stopped by a signal reports the code its shell gives for one.
started_atalways
string
RFC 3339 timestamp.
GET/computers/{id}/executions/{executionId}
Read execution metadatamember+
Reads daemon memory only, without guest commands, files, watcher setup or automatic resume. Only background exec participates; synchronous results have no retrievable execution ID yet. The ID comes from the 202 response and both exited/lost process.exited payloads. IDs cannot resolve to a replacement command when a PID is reused. Current computer scope and the original execution scope must both match. No command text or output is included. Handles are ephemeral: daemon restart, computer removal, PID replacement or cleanup makes an ID unavailable. Observed exits expire after ten minutes. Unknown and expired IDs both return 404 with code execution_unavailable, never an empty successful result. Lost may be observed briefly before the handle is removed.
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/executions/exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4' \
-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"
executionId = "exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
c = client.computers.get(id)
result = c.execution(executionId)
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 executionId = "exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4";
const c = await client.computers.get(id);
const result = await c.execution(executionId);
console.log(JSON.stringify(result, null, 2));
Explicit guest-file reads with caller-owned byte offsets. No shared cursor or diagnostic is consumed. Retry the same offsets after a lost response; reads never replay a command or automatically resume the computer. A stopped or suspended computer returns 409. A missing log returns 409 with code output_unavailable, not empty output. Unknown, expired, replaced or lost handles return 404 with code execution_unavailable. Empty success means the existing files had no bytes at the supplied positions. This route reads volatile, mutable guest paths: deletion, truncation, replacement or tampering can change bytes between retries. It is not suitable for passive Activities/history; explicitly capture retained-output for immutable later reads. Each query key may appear once. Offsets and limit must be nonnegative decimal integers; each offset plus limit must be at most 9007199254740991. Responses are no-store.
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/executions/exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4/output?stdout_offset=0&stderr_offset=0' \
-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"
executionId = "exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
query = {"stdout_offset":"0","stderr_offset":"0"}
c = client.computers.get(id)
result = c.execution_output(executionId, stdout_offset=int(query["stdout_offset"]), stderr_offset=int(query["stderr_offset"]))
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 executionId = "exec_a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4";
const query = {"stdout_offset":"0","stderr_offset":"0"};
const c = await client.computers.get(id);
const result = await c.executionOutput(executionId, {stdoutOffset: Number(query.stdout_offset), stderrOffset: Number(query.stderr_offset)});
console.log(JSON.stringify(result, null, 2));
The opaque execution_id from a backgrounded exec (exec_ followed by 32 lowercase hex characters).
Query parameters
stdout_offsetrequired
integer
Explicit stdout byte offset. Start at 0; offset + limit must remain a safe JavaScript integer.
stderr_offsetrequired
integer
Explicit independent stderr byte offset. Start at 0.
limit
integer
Maximum decoded bytes per stream, not including the separate 64 KiB diagnostic.
Response
Bounded raw bytes and next independent offsets.
Response fields
execution_idalways
string
The requested stable execution ID.
stdout_b64always
string
Raw stdout bytes, base64; at most limit decoded bytes.
stderr_b64always
string
Raw stderr bytes, base64; at most limit decoded bytes.
stdout_offsetalways
integer
Next stdout byte offset: supplied offset plus decoded bytes returned.
stderr_offsetalways
integer
Next stderr byte offset, independent of stdout and all other readers.
stdout_morealways
boolean
The read reached its limit without observing EOF. Another read may still be empty.
stderr_morealways
boolean
The same for stderr. False is a current EOF observation, not a promise of no future output.
diagnostic_b64always
string
Separate daemon-captured wrapper diagnostic, base64, at most 65536 decoded bytes. Repeated in full on every read; not part of either guest-file offset. Legacy reads cannot consume it.
diagnostic_truncatedalways
boolean
The wrapper diagnostic exceeded 65536 bytes or guest-agent capture was truncated; the diagnostic is incomplete.
GET/computers/{id}/windows
List windowsviewer+
What is on the desktop, as a list rather than a picture — id, title, class, type, geometry and which one has focus. Cheaper and more reliable than asking a model to find a window in a screenshot.
A Windows computer answers 400 with reason: "unsupported": listing windows is not supported on Windows guests yet. A stopped or suspended computer answers 409 with reason: "unavailable", since this read does not resume one: start it first.
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/windows' \
-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.windows()
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.windows();
console.log(JSON.stringify(result, null, 2));
Include the desktop’s own furniture — panels, docks, the wallpaper window. Left out by default because none of it is a window a caller wants to act on.
Focus, raise, minimize, maximize, unmaximize, close, move or resize one window. On a Wayland desktop a tiled window cannot be moved or resized — float it first — and an action the compositor refuses is a 400. If the guest accepts the action but does not report its result before the deadline, this answers 504 without a reason. The action may already have happened, so do not treat that uncertain outcome as permission to repeat it. An image without the window tools (xdotool on an X11 desktop, python3 on a Wayland one) answers 400 with reason: "unsupported": listing its windows still works, and acting on them needs a computer created from an image that includes the tool. Do not retry it.
A suspended computer is resumed to act on the window, after the action, the window id and the guest OS have been checked: a malformed action, or any action on a Windows guest, is refused without resuming or billing 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. 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/windows/0x3200007' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"action":"focus"}'
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"
window = "0x3200007"
c = client.computers.get(id)
result = c.window_action(window, "focus")
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 window = "0x3200007";
const c = await client.computers.get(id);
const result = await c.windowAction(window, "focus");
console.log(JSON.stringify(result, null, 2));
The window as it now is, or null when the action left nothing to describe.
gone
boolean
The window closed. This is what separates the two outcomes that have no window to show: true means it is gone, which is what a close is for, while false with no window means the action happened and the guest could not describe the result.
Failures
503
Also, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.
GET/computers/{id}/clipboard
Read the desktop clipboardmember+
What is on the clipboard of the computer’s desktop — the CLIPBOARD selection, which is what Ctrl-C writes and Ctrl-V pastes.
This reads the selection out of the console user’s graphical session, so it needs no reboot and no permission from a browser — which is what makes it the route to build on if you want to write it once. The other way text crosses — RFB extended cut text over the desktop websocket — is faster and live, and needs a virtio-serial channel a computer only acquires on a COLD boot; see Vnc.url.
It does have ONE requirement of the image, and it is a 400 rather than something that clears: the guest needs xclip on an X11 desktop, or wl-clipboard (wl-paste/wl-copy) on a Wayland one. Every golden built since August 2026 carries it, so in practice this is a computer created before then — and a computer keeps the image it was created from, so the fix is to install that tool in the guest, which you can do yourself since you have root there, or to create a new computer from an image that includes it. The refusal says so in as many words, and carries reason: "unsupported". Do not retry it: unlike the 409s below it will never start working.
It is a READ, not a subscription. Nothing here notices a Ctrl-C in the guest on its own, and this call does not resume a suspended computer: what somebody copied is not worth waking a machine for. A computer that is stopped or suspended answers 409 with reason: "unavailable" — the one 409 here that never clears by waiting, because starting the computer is something only you can do. Some others carry starting (the guest agent has not answered inside its boot window yet) or contention (its guest agent is busy with another call), and both of those are worth sending again. Desktop-session and X-server failures carry no reason: the platform cannot distinguish a guest still booting from a logged-out desktop, crashed window manager, or persistently unreachable display, so it does not guess that retrying will help. Switch on reason, not on the sentence.
At most 128 KiB comes out, and more than that is refused with a 413 rather than truncated — a half a password is not less of an answer, it is a wrong one that looks completely normal. The write cap is different and smaller; see the PUT.
Needs the member role despite being a read, for the reason GET /files does: what somebody last copied is not a read of anything this platform models, and a key or a password is the ordinary case rather than the unlucky one.
Windows guests are refused outright — there is no way to run anything in the interactive session there yet.
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/clipboard' \
-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.clipboard()
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.clipboard();
console.log(JSON.stringify(result, null, 2));
What is on the desktop’s CLIPBOARD selection right now — the one Ctrl-C writes to, not the X PRIMARY selection that middle-click pastes.
An empty desktop clipboard is "", and it is not distinguishable from one holding an empty string, because X does not distinguish them either.
PUT/computers/{id}/clipboard
Set the desktop clipboardmember+
Puts text on the desktop’s clipboard, ready to paste. Pair it with POST /computers/{id}/input sending ctrl+v to get the text into whatever has focus — this call alone leaves it on the clipboard and touches nothing on screen.
Unlike the read, this DRIVES the computer: a suspended one is resumed to serve it, which is a start and is charged like one.
At most 64 KiB goes in — half the read cap, and the difference is not taste. The text travels to the guest inside one argument of one command, and Linux caps a single argument at 128 KiB; two layers of base64 stand between your text and that ceiling, so each byte costs about 1.8 of it. Anything larger is a 413.
A NUL byte is refused, because the write is confirmed through a shell and a shell truncates at the first one — so it would land and then be reported as never having landed.
Empty text CLEARS the clipboard, and it is the only way to say that. Sending "" leaves the desktop holding nothing; sending something else is not a clear, it is a different secret in the same place. Omitting text altogether is still a 400 — an absent field is a malformed request, an empty one is an instruction.
Two other 400s here NEVER clear, and they are the ones not to retry. The guest needs xclip (X11) or wl-clipboard (Wayland) in its image — every golden built since August 2026 has it, so this is a computer created before then, and a computer keeps the image it was created from; install that tool in the guest, which you can do since you have root there, or create a new computer from an image that includes it. And Windows guests are refused outright. Both say which they are, and both carry reason: "unsupported" — the word for a refusal no action on the computer changes. This matters more on this operation than on the read, because the 409 advice below tells you to retry some failures and neither of these is one of them.
The write is confirmed by reading the selection back rather than by an exit status, so a 200 here means the desktop is holding your text, not merely that a command ran.
Not every 409 here is worth retrying, and reason in the body is how you tell.contention is the one that clears by itself — "the desktop did not take the text" means something else claimed the selection in that instant, a clipboard manager settling, usually — and starting means the guest agent has not answered inside its boot window yet, which clears too, just more slowly. Desktop-session and X-server failures are deliberately unclassified because they can also mean nobody is logged in, the window manager crashed, or the display remains unreachable; an absent reason gives no retry advice. unavailable is the one that does not: the computer is not running, and starting it is the fix rather than another attempt. Branch on that word and never on the sentence, which is prose and is rewritten. A blanket retry on 409 spins until your deadline against a computer that is simply stopped, spending your request allowance a turn at a time and never coming good.
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/clipboard' \
-H "Authorization: Bearer $MANDALA_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{"text":"https://mandala.computer"}'
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("{\"text\":\"https://mandala.computer\"}")
c = client.computers.get(id)
result = c.set_clipboard(body["text"])
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 = {"text":"https://mandala.computer"};
const c = await client.computers.get(id);
const result = await c.setClipboard(body.text);
console.log(JSON.stringify(result, null, 2));
The text to put on the clipboard, at most 64 KiB. Required; empty clears the clipboard.
Response
The desktop is holding the text.
Response fields
ok
boolean
Always true. A failure is a status code, not ok: false.
Failures
503
Also, on a suspended computer this request would resume, a sealed session the platform could not open or carry just then — the service that holds its key did not answer, or the host failed moving the saved session. Nothing ran in the guest. Read the computer’s status, then send it again; it usually clears in a moment.