Descanto Docs
Concepts

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 (201 is only for the request that actually created it).
  • Mutations (wake/hibernate/destroy) scope the key to (desktop, key): a repeat request returns the existing Operation handle 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.

On this page