Quickstart
Create a desktop, wake it, run a command, and hibernate it.
This walks through the full desktop lifecycle once, end to end, with every
step shown three ways: raw curl, the TypeScript SDK,
and MCP tools. Pick whichever matches how you're integrating.
Operation handles
wake, hibernate, and destroy are asynchronous: they return an
operation handle rather than blocking until the change is done. This
quickstart uses ?wait=true (curl) or the SDK's default wait: true
behavior to keep things linear -- see
Concepts: Operations for the handle/polling
model itself.
1. Get access
Descanto is early access: desktops are manually provisioned. Log in to the
dashboard with your email (a one-time code is emailed to you -- no
password, no OAuth redirect), then create an API key from the API
keys page. You can also create one directly against the API with a
dashboard session: POST /v1/keys. Export the key:
export CANTO_API_KEY="canto_sk_..."The TypeScript SDK and the MCP server both read CANTO_API_KEY from the
environment automatically. For raw curl, send it as a bearer credential on
every request:
BASE=http://127.0.0.1:8081/v1
AUTH="Authorization: Bearer $CANTO_API_KEY"(http://127.0.0.1:8081 is controld's own local-dev address and the
SDK's default baseUrl -- this will change once a hosted Descanto API is
live; your API key request will come with the right base URL.)
2. Create a desktop
DESKTOP=$(curl -sS -X POST "$BASE/desktops" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{"tier": "small", "billing_mode": "hourly"}')
DESKTOP_ID=$(echo "$DESKTOP" | jq -r .id)
echo "$DESKTOP"tier is required (small/default/large); billing_mode defaults to
hourly when omitted. A 201 means a new desktop was created; a 200
means an Idempotency-Key you sent was already used and you got the
existing desktop back instead. See
POST /v1/desktops.
Bootstrap it with env vars and a setup script
Optionally, bootstrap the desktop on its first wake with environment
variables and a setup script -- useful for agent workloads that need
credentials or tooling installed before the first exec:
curl -sS -X POST "$BASE/desktops" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{
"tier": "small",
"env": {"APP_ENV": "staging"},
"setup_script": "#!/bin/sh\napt-get update && apt-get install -y ripgrep\n"
}'env is written to /root/.canto/env in the guest and setup_script runs
once, both on the desktop's first successful wake -- see
Concepts: env and setup_script
for the caps and charset rule.
3. Wake it
A freshly created desktop starts provisioning/hibernated, not awake --
wake it before running anything inside the guest.
curl -sS -X POST "$BASE/desktops/$DESKTOP_ID/wake?wait=true" -H "$AUTH"?wait=true long-polls server-side (2s cadence, capped at 120s) until the
operation settles, and still returns 200/202 either way -- never a
timeout error. Omit it to get the operation handle back immediately
(202) and poll GET /v1/operations/{id}
yourself.
4. Run a command
curl -sS -X POST "$BASE/desktops/$DESKTOP_ID/exec" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{"command": "echo hello", "timeout_secs": 10}'Returns {"exit_code", "stdout", "stderr"} once the command finishes
inside the guest. timeout_secs defaults to 60s and is clamped (never
rejected) to 300s. See
exec and files.
Long-running commands: run detached
A blocking exec is bounded by timeout_secs (60s default, clamped to
300s max). For anything longer -- a build, a test suite, a background
server -- run it detached instead and poll for status:
# Spawn it -- returns immediately with a process_id.
PROC=$(curl -sS -X POST "$BASE/desktops/$DESKTOP_ID/exec" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{"command": "sleep 60 && echo done", "detached": true}')
PROCESS_ID=$(echo "$PROC" | jq -r .process_id)
# Poll it whenever you want to check in.
curl -sS "$BASE/desktops/$DESKTOP_ID/processes/$PROCESS_ID" -H "$AUTH"A detached process is killed when the desktop hibernates -- see Concepts: background processes.
5. Work with files
# Write (path is given without its leading slash).
curl -sS -X PUT "$BASE/desktops/$DESKTOP_ID/files/home/user/task.txt" \
-H "$AUTH" --data-binary "hello from canto"
# Read it back.
curl -sS "$BASE/desktops/$DESKTOP_ID/files/home/user/task.txt" -H "$AUTH"Writes are capped at 8 MiB (413 past that). See
exec and files.
6. Watch the screen
curl -sS -X POST "$BASE/desktops/$DESKTOP_ID/stream" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{"claim": "view"}'Both return a dial-able tcp://host:port/?ticket=... URL and its expiry --
a raw stream ticket, not an embeddable web player. A hosted
viewer/embed integration is coming; today, point a stream-capable client at
the ticket URL directly. See
Streaming for the full claim/takeover model.
7. Hibernate
curl -sS -X POST "$BASE/desktops/$DESKTOP_ID/hibernate?wait=true" -H "$AUTH"Hibernating preserves the desktop's full disk state and stops billing compute -- see Billing for exactly how awake-time is metered. Wake it again any time with the same call from step 3.
Fork it
A hibernated desktop can be cloned into up to 20 independent copies with
POST /v1/desktops/{id}/fork:
curl -sS -X POST "$BASE/desktops/$DESKTOP_ID/fork?wait=true" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d '{"count": 3, "acknowledge_shared_state": true}'Forks come back hibernated -- wake the ones you need. See
Concepts: Forking for the full model,
including why acknowledge_shared_state has no default.
Concepts: Operations
Operation handles, polling, ?wait, idempotency, and background processes.
Concepts: Forking
Clone a hibernated desktop into N independent copies.
API reference
Every route and status code.
Beyond the basics
Once wake/exec/files/hibernate feels familiar, the same awake desktop supports a few more surfaces: