Descanto Docs

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.

Beyond the basics

Once wake/exec/files/hibernate feels familiar, the same awake desktop supports a few more surfaces:

On this page