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/v1This 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.
| Status | When |
|---|---|
200 OK | POST /v1/desktops with an Idempotency-Key already used by this org -- returns the existing desktop, not a new one. |
400 Bad Request | Malformed 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 Unauthorized | Missing/non-Bearer/garbage Authorization header, or a key/JWT that fails WorkOS verification. |
404 Not Found | The 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 Conflict | Stale 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 Large | A 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 Unavailable | A 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 Timeout | A guest op (exec/files) that ran past its own deadline inside the guest -- a normal, retryable outcome, not a server bug. |
500 Internal Server Error | Anything 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 --413past that. exectimeouts default to 60s and are clamped (never rejected) to a 300s maximum, regardless of whattimeout_secsrequests. Ignored entirely for adetachedexec -- 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 viatail_bytes. envon 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_scripton desktop create is capped at 64 KiB.forkcount must be in 1..=20 per request.?wait=truelong-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 --413past that; passregion/scale_percent. See Computer use. - Input batches (
POST .../input) are capped at 50 actions, 32 KiB total text, and 10s totalwaittime 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, withv1continuing to run alongside it. - Deprecation signaling. When an endpoint or a whole version is
scheduled for removal, responses from it will carry the standard
Deprecationheader and, once a shutdown date is set, aSunsetheader 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 Requestsasapplication/problem+json(same envelope as every other error), with aRetry-Afterheader 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 a429. - 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.