Descanto Docs
API Reference

Exec and files

Run commands (blocking or detached), poll processes, and read/write/list files on an awake desktop's guest.

The routes below only work while the desktop is awake -- a 409 means the desktop isn't awake, or its control claim is already held by another client (see Streaming for claims).

Run a command

POST /v1/desktops/{id}/exec

ParamInRequiredDescription
idpathyesDesktop id

Request body (ExecBody):

{ "command": "echo hello", "timeout_secs": 10, "detached": false }
FieldTypeRequiredDescription
commandstringyes
timeout_secsinteger | nullnoDefaults to 60s when omitted; clamped (never rejected) to 300s. Ignored when detached: true.
detachedbooleannoIf true, the command is spawned in the guest and this route answers 202 {process_id} instead of waiting for it to exit. Defaults to false (blocking).
curl -sS -X POST "$BASE/desktops/$DESKTOP_ID/exec" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"command": "echo hello", "timeout_secs": 10}'

Response (ExecResponse, detached: false):

{ "exit_code": 0, "stdout": "hello\n", "stderr": "" }
StatusMeaning
200detached: false (default): the command ran to completion in the guest
202detached: true: the command was spawned in the guest; poll GET .../processes/{process_id} for its status
400Malformed id/body, or empty command
401Missing/invalid credentials
404Desktop not found or belongs to another org
409Desktop is not awake, or its control claim is already held
503The desktop's host is currently unreachable -- retry
504The command timed out inside the guest -- retry

Run a command, detached

Pass "detached": true to spawn a long-running command in the background instead of blocking on it -- see Concepts: background processes for the full model.

curl -sS -X POST "$BASE/desktops/$DESKTOP_ID/exec" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"command": "sleep 300 && echo done > /tmp/marker", "detached": true}'

Response (DetachedExecResponse, 202):

{ "process_id": "p_..." }

Poll a detached process

GET /v1/desktops/{id}/processes/{process_id}

ParamInRequiredDescription
idpathyesDesktop id
process_idpathyesProcess id returned by a prior detached POST .../exec
tail_bytesquerynoHow many trailing bytes of stdout/stderr to return. Omitted/0 means hostd's own default (64 KiB); capped at 512 KiB.
curl -sS "$BASE/desktops/$DESKTOP_ID/processes/$PROCESS_ID?tail_bytes=65536" -H "$AUTH"

Response (ProcessStatusResponse):

{
  "process_id": "p_...",
  "status": "running",
  "exit_code": 0,
  "stdout_tail": "",
  "stderr_tail": ""
}
FieldTypeDescription
process_idstring
statusstringrunning | exited | lost
exit_codeintegerValid only when status == "exited"
stdout_tailstringTrailing tail_bytes of stdout captured so far
stderr_tailstringTrailing tail_bytes of stderr captured so far
StatusMeaning
200The process's current status
400Malformed id, or a non-numeric tail_bytes
401Missing/invalid credentials
404Desktop not found (or belongs to another org), or process_id is unknown
409Desktop is not awake
503The desktop's host is currently unreachable -- retry

Read a file

GET /v1/desktops/{id}/files/{path}

ParamInRequiredDescription
idpathyesDesktop id
pathpathyesAbsolute guest path, without its leading / (e.g. home/user/notes.txt for /home/user/notes.txt)
offsetquerynoFiles v2 chunked read: return the byte window starting here instead of the whole file. The 200 response then carries X-Canto-File-Size (total size) and X-Canto-Eof (true/false) headers; an offset at or past EOF is an empty 200 body with X-Canto-Eof: true.
lengthquerynoWindow length for a chunked read (requires offset); defaults to (and is capped at) 8 MiB -- 400 above the cap.
curl -sS "$BASE/desktops/$DESKTOP_ID/files/home/user/notes.txt" -H "$AUTH"

# Chunked read of a large file:
curl -sS "$BASE/desktops/$DESKTOP_ID/files/home/user/big.log?offset=0&length=8388608" \
  -H "$AUTH" -D -

Response is 200 with application/octet-stream -- the whole file, or (with offset) the requested window.

StatusMeaning
200Raw file content (the requested window, for a chunked read)
400Malformed id, a path that's empty/contains a NUL byte/contains a .. segment, non-numeric offset/length, length without offset, or length over 8 MiB
401Missing/invalid credentials
404Desktop not found (or belongs to another org), or the file itself does not exist
409Desktop is not awake, or its control claim is already held
413Whole-file read of a file over 8 MiB -- switch to chunked reads (?offset=/length=)
503The desktop's host is currently unreachable -- retry
504The read timed out inside the guest -- retry

Write a file

PUT /v1/desktops/{id}/files/{path}

ParamInRequiredDescription
idpathyesDesktop id
pathpathyesAbsolute guest path, without its leading /
appendquerynoFiles v2 chunked upload: append the body iff the file's current size equals offset (which becomes required with append=true). A size mismatch is a 409 whose problem body carries a currentSize extension member, making retried chunks idempotent.
offsetquerynoRequired with append=true: the byte offset this chunk expects the file to currently end at.

Request body: raw application/octet-stream file content (one chunk, when appending).

curl -sS -X PUT "$BASE/desktops/$DESKTOP_ID/files/home/user/notes.txt" \
  -H "$AUTH" --data-binary "hello from canto"

# Chunked upload of a large file -- first chunk, then append at offset:
curl -sS -X PUT "$BASE/desktops/$DESKTOP_ID/files/home/user/big.bin" \
  -H "$AUTH" --data-binary @chunk0
curl -sS -X PUT "$BASE/desktops/$DESKTOP_ID/files/home/user/big.bin?append=true&offset=8388608" \
  -H "$AUTH" --data-binary @chunk1
StatusMeaning
204File written (or chunk appended)
400Malformed id, a path that's empty/contains a NUL byte/contains a .. segment, append=true without offset, or non-numeric params
401Missing/invalid credentials
404Desktop not found or belongs to another org
409Desktop is not awake, its control claim is already held, or an append's size != offset guard failed (body carries currentSize)
413Body exceeds the 8 MiB per-request limit
503The desktop's host is currently unreachable -- retry
504The write timed out inside the guest -- retry

Delete a file

DELETE /v1/desktops/{id}/files/{path}

ParamInRequiredDescription
idpathyesDesktop id
pathpathyesAbsolute guest path, without its leading /. The filesystem root itself (/, /.) is rejected 400.
recursivequerynoDelete a directory tree. Without it, deleting a directory is a 409.
curl -sS -X DELETE "$BASE/desktops/$DESKTOP_ID/files/home/user/notes.txt" -H "$AUTH"
curl -sS -X DELETE "$BASE/desktops/$DESKTOP_ID/files/home/user/tmp?recursive=true" -H "$AUTH"
StatusMeaning
204Deleted
400Malformed id, an invalid path (empty/NUL/..), or the filesystem root
401Missing/invalid credentials
404Desktop not found (or belongs to another org), or the path itself does not exist (a second DELETE of the same path 404s)
409Desktop is not awake, or the path is a directory and recursive was not set
503The desktop's host is currently unreachable -- retry
504The delete timed out inside the guest -- retry

Stat a file

GET /v1/desktops/{id}/files-meta/{path}

ParamInRequiredDescription
idpathyesDesktop id
pathpathyesAbsolute guest path, without its leading /
curl -sS "$BASE/desktops/$DESKTOP_ID/files-meta/home/user/notes.txt" -H "$AUTH"

Response (FileStatJson):

{
  "path": "home/user/notes.txt",
  "fileType": "file",
  "sizeBytes": 17,
  "modifiedAt": "2026-08-20T12:00:00Z",
  "mode": "644"
}

fileType is "file" | "directory" | "symlink" | "other"; mode is the permission bits as octal text.

StatusMeaning
200The path's metadata
400Malformed id, or a path that's empty/contains a NUL byte/contains a .. segment
401Missing/invalid credentials
404Desktop not found (or belongs to another org), or the path itself does not exist
409Desktop is not awake
503The desktop's host is currently unreachable -- retry
504The stat timed out inside the guest -- retry

List a directory

GET /v1/desktops/{id}/dir/{path}

ParamInRequiredDescription
idpathyesDesktop id
pathpathyesAbsolute guest directory path, without its leading /. The guest root is listed as GET .../dir/ (a trailing slash, empty capture) -- . works too, but URL normalization collapses it to the same thing.
curl -sS "$BASE/desktops/$DESKTOP_ID/dir/home/user" -H "$AUTH"

Response (ListDirResponse) -- direct children, sorted by name (byte order), capped at 2000 entries:

{
  "path": "home/user",
  "entries": [
    { "name": "notes.txt", "fileType": "file", "sizeBytes": 17, "modifiedAt": "2026-08-20T12:00:00Z" }
  ],
  "truncated": false
}

truncated is true when the listing was cut at the 2000-entry cap (or lost records to the guest-side output bound).

StatusMeaning
200The directory's direct children
400Malformed id, or a path that's empty/contains a NUL byte/contains a .. segment
401Missing/invalid credentials
404Desktop not found (or belongs to another org), or the path itself does not exist
409Desktop is not awake, or the path is not a directory
503The desktop's host is currently unreachable -- retry
504The listing timed out inside the guest -- retry

On this page