Operations
Async handles, polling, ?wait, idempotency, and background processes.
wake, hibernate, and destroy don't apply their change synchronously.
Each returns an operation handle -- a small, pollable record of an
in-flight (or already-settled) state transition:
{
"id": "5b1e...",
"desktop_id": "d_...",
"kind": "wake",
"state": "pending",
"result": null,
"error": null
}Fetch the current state of any operation with
GET /v1/operations/{id}:
curl -sS "$BASE/operations/$OPERATION_ID" -H "$AUTH"?wait=true: server-side long-poll
Every mutating route accepts an optional ?wait=true query parameter. When
set, the server long-polls the operation itself (2s cadence, capped at
120s) before responding -- so the caller gets the settled operation back
in the same request/response cycle, without writing their own poll loop:
curl -sS -X POST "$BASE/desktops/$ID/wake?wait=true" -H "$AUTH"The response is always 200/202, never a timeout error -- if the
120s cap is reached while the operation is still pending/running, you
just get that in-progress operation back and can keep polling
GET /v1/operations/{id} yourself. A settled failed operation observed
within the ?wait=true window is surfaced as 409 Conflict instead of a
200, with operation_id set on the problem body.
Without ?wait=true, a mutation returns 202 Accepted immediately with the
freshly enqueued operation (state: "pending"), and it's on you to poll.
The TypeScript SDK defaults to wait: true and adds
its own client-side re-poll fallback beyond the server's 120s cap -- see
that page's "wait vs handles" section.
Idempotency
POST /v1/desktops (create) and the three lifecycle mutations all accept an
optional Idempotency-Key header:
curl -sS -X POST "$BASE/desktops" \
-H "$AUTH" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: my-client-generated-key' \
-d '{"tier": "small"}'- Create scopes the key to
(org, key): a repeat request with the same key from the same org returns the existing desktop (200), never a second one (201is only for the request that actually created it). - Mutations (
wake/hibernate/destroy) scope the key to(desktop, key): a repeat request returns the existingOperationhandle instead of enqueueing a duplicate.
Reusing the same key value across two different orgs (or two different
desktops, for mutations) is safe -- the scoping is never global, so it
creates two independent rows rather than colliding. A non-ASCII
Idempotency-Key is rejected with 400 Bad Request.
Retry-safe by default
Because create/mutation idempotency keys are scoped per-org/per-desktop,
it's safe to generate a fresh UUID per logical call and attach it
automatically to every retry of that call -- which is exactly what the
TypeScript SDK does (crypto.randomUUID()) when you don't supply one
yourself.
Background processes (detached exec)
Control-plane operations (above) aren't the only pollable, async thing in
the API. POST /v1/desktops/{id}/exec
accepts "detached": true to spawn a command in the guest and hand back a
process_id instead of blocking until it exits -- useful for anything
longer than the request's own timeout budget (builds, long test runs,
servers you want running in the background).
curl -sS -X POST "$BASE/desktops/$ID/exec" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{"command": "long-running-task.sh", "detached": true}'
# -> 202 {"process_id": "p_..."}Poll it with GET /v1/desktops/{id}/processes/{process_id}:
curl -sS "$BASE/desktops/$ID/processes/$PROCESS_ID" -H "$AUTH"{
"process_id": "p_...",
"status": "running",
"exit_code": 0,
"stdout_tail": "...",
"stderr_tail": ""
}status is one of running, exited, or lost (exit_code is only
meaningful once status == "exited"). stdout_tail/stderr_tail are the
trailing bytes captured so far -- not the full output -- sized by the
optional tail_bytes query param (default 64 KiB, capped at 512 KiB).
Killed on hibernate
A detached process is a property of the running guest, not of the desktop's persisted state. Hibernating the desktop kills it -- there is no "resume on wake." If you need a task to survive a hibernate/wake cycle, checkpoint its progress to disk yourself (or to a file you read back after waking) rather than relying on the process still being alive.