Descanto Docs
API Reference

API reference overview

Base URL, auth, error format, limits, versioning and deprecation policy, and rate-limit conventions for the Descanto API.

Hand-written reference

These API pages are hand-written from the committed OpenAPI 3.1 contract (canto/controld/openapi/v1.json), rather than generated with fumadocs-openapi. See the note at the bottom of this page for why.

Base URL

http://127.0.0.1:8081/v1

This is controld's own local-dev default (and the TypeScript SDK's default baseUrl) -- there is no fixed production hostname yet. Your API key request will come with the right base URL for a hosted deployment. The OpenAPI document itself declares no fixed server ("servers": [{"url": "/"}]): it's served at the root of whatever host runs controld.

The identical OpenAPI document is also served live, unauthenticated, at GET /v1/openapi.json.

Auth

Every route under /v1 (except GET /v1/openapi.json) requires:

Authorization: Bearer <credential>

<credential> is either a canto_sk_... API key or a WorkOS AuthKit JWT access token. See Concepts: Orgs and auth for the two forms and org-scoping semantics.

Error format

Every non-2xx response is an RFC 9457 ("Problem Details for HTTP APIs") body, served as application/problem+json:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "desktop not found"
}

operation_id is present in addition to the fields above when the problem concerns a specific operation -- e.g. a ?wait=true poll that observed the operation settle failed.

StatusWhen
200 OKPOST /v1/desktops with an Idempotency-Key already used by this org -- returns the existing desktop, not a new one.
400 Bad RequestMalformed JSON body, an invalid tier/billing_mode/desktop or operation id, a non-numeric/out-of-range start/end on GET /v1/usage, a non-true/false ?wait= value, a non-ASCII Idempotency-Key, or a ..-traversing files path.
401 UnauthorizedMissing/non-Bearer/garbage Authorization header, or a key/JWT that fails WorkOS verification.
404 Not FoundThe desktop/operation truly doesn't exist, or it belongs to a different org than the caller's -- these two cases are byte-identical on purpose. See org scoping.
409 ConflictStale expected_generation, wrong desktop state for the requested transition, a guest op against a desktop that isn't currently awake, or a stream control claim already held by another client.
413 Payload Too LargeA files PUT body over the 8 MiB cap, POST /v1/desktops/{id}/fork when hostd has no free capacity for the requested count (surfaced on the settled operation unless ?wait=true), or any request body over axum's own 2 MiB default limit.
503 Service UnavailableA mutation (wake/hibernate/destroy/fork) sent to a non-leader replica; a guest op whose desktop's host is currently unreachable (retryable); or WorkOS itself is unreachable.
504 Gateway TimeoutA guest op (exec/files) that ran past its own deadline inside the guest -- a normal, retryable outcome, not a server bug.
500 Internal Server ErrorAnything else (a DB error, an unmapped internal failure) -- the body never carries the real error text.

503/504 mean "this exact request is safe to retry" (a transient/timing condition); 500 means "something is actually broken server-side." The TypeScript SDK's default retry policy acts on the former, not the latter.

Limits

  • File writes (PUT /v1/desktops/{id}/files/{path}) are capped at 8 MiB -- 413 past that.
  • exec timeouts default to 60s and are clamped (never rejected) to a 300s maximum, regardless of what timeout_secs requests. Ignored entirely for a detached exec -- see exec and files.
  • Detached process output tails (GET .../processes/{process_id}) default to hostd's own 64 KiB and are capped at 512 KiB via tail_bytes.
  • env on desktop create is capped at 100 keys / 64 KiB total serialized size, and every key must match ^[A-Za-z_][A-Za-z0-9_]*$.
  • setup_script on desktop create is capped at 64 KiB.
  • fork count must be in 1..=20 per request.
  • ?wait=true long-polls at a 2s cadence, capped at 120s per request.
  • Every request body is additionally capped at axum's own default 2 MiB limit (independent of the 8 MiB file-write cap above).
  • Screenshots (POST .../screenshot) are capped at 8 MiB -- 413 past that; pass region/scale_percent. See Computer use.
  • Input batches (POST .../input) are capped at 50 actions, 32 KiB total text, and 10s total wait time per batch.
  • Files v2 chunked reads/writes (?offset=/?length=/?append=) cap each window/chunk at 8 MiB, same as a whole-file write. Directory listings (GET .../dir/{path}) are capped at 2000 entries. See Exec and files.
  • Webhook endpoint URLs are capped at 2000 chars, must be https://, and can't target a loopback/link-local host. Delivery-log reads (?limit=) default to 50, capped at 200. See Webhooks.

Versioning and deprecation

The API is versioned in the URL path: every route lives under /v1, and the OpenAPI document you integrate against is the committed contract for that version. Within v1:

  • Additive changes only. New endpoints, new optional request fields, and new response fields may appear without notice. Parse responses leniently: unknown fields must be ignored.
  • No breaking changes without a deprecation window. Removing or renaming a field, changing a type, or changing an endpoint's semantics will not happen inside v1. Behavior like that ships as /v2, with v1 continuing to run alongside it.
  • Deprecation signaling. When an endpoint or a whole version is scheduled for removal, responses from it will carry the standard Deprecation header and, once a shutdown date is set, a Sunset header with that date. The deprecation will also be announced in the changelog with at least 90 days notice before sunset. Nothing in the API is deprecated today, so you won't see these headers yet.

Treat the absence of Deprecation/Sunset headers as "safe to build on."

Rate limiting

Early-access deployments are provisioned per org and not currently rate-limited at the HTTP layer, so you won't see 429s under normal use. The conventions below are the contract for when limits are enforced:

  • A throttled request returns 429 Too Many Requests as application/problem+json (same envelope as every other error), with a Retry-After header saying how many seconds to wait.
  • Rate-limited responses will carry the IETF RateLimit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) so clients can self-throttle before hitting a 429.
  • Until then, the practical limits are the per-request caps in Limits and your org's desktop quota.

If you're building a client today: honor Retry-After on any 429 or 503, and back off exponentially when neither header is present.

Why hand-written, not fumadocs-openapi

fumadocs-openapi can generate reference pages (and an interactive "try it" panel) directly from an OpenAPI document, but its request-proxy route for the try-it panel is a live server function -- not a fit for this site's static export (output: "export", deployed as static files to Cloudflare Pages, no server runtime). The pages in this section are hand-written directly from canto/controld/openapi/v1.json instead: every parameter, request/response schema, and status code below is transcribed from that committed contract, one page per resource group.

On this page