Skip to content
DocsAll docs
Console

Capsules API

Lists the API routes that create capsules, run commands in them, and read the run records.

Last updated

On this page

The capsule document

Every route that returns a capsule returns this object. The record outlives the capsule.

json
{  "id": "cap-3c4d5e6f7a8b9c0d",  "owner": "karthik",  "team": "",  "region": "centralindia",  "image": "ghcr.io/kloudlite/kloudlite-workspace",  "packages": ["python3"],  "phase": "running",  "reason": null,  "message": null,  "created_at": "2026-10-18T09:30:00Z",  "started_at": "2026-10-18T09:30:12Z",  "finished_at": null,  "timeout_secs": 300,  "apiToken": null,  "cmd": ["python3", "main.py"],  "tty": false,  "stdin": false,  "exit_code": null,  "network": "internet",  "cpu": 1,  "memory_gb": 2,  "from": null,  "desk": null,  "snapshots": [],  "basedOn": null}
FieldTypeMeaning
idstringcap- and 16 hex digits
owner, teamstringthe owner's handle; the team slug, "" for a personal capsule
regionstringthe region it runs in
image, packagesstring, string[]what it was created with
phasestringpending, starting, running, or terminated. A capsule never stops and starts again
reasonstring or nullwhy it ended, once terminated: Terminated (you asked), Timeout, Exited (the main process ended; read exit_code), Failed (the pod failed or the create failed), Unplaceable (no node could take it), SourceGone (the bytes it started from are gone), Lost (its node stopped running it), EnvironmentGone
messagestring or nullthe detail behind reason
created_at, started_at, finished_atstring or nullRFC 3339. started_at is the container's first ready moment; the timeouts count from it
timeout_secsintegerthe lifetime limit
apiTokenobject or null{ "name", "id8" } of the API token that created the capsule, else null. The console shows API token {name} and never the creator's handle
cmd, tty, stdinstring[], boolean, booleanthe main process as created
exit_codeinteger or nullthe main process's exit code, once it exited on its own
networkstringinternet or none
cpu, memory_gbintegerthe size
fromobject or null{"workspace": id} or {"snapshot": id} when it started as a copy
deskstring or nullthe desk that manages it
snapshotsstring[]snapshots pushed from it; they outlive it
basedOnstring or nullfor a from_workspace start, the cut it began from, and its age, for example clone-… (0s old)

POST /v1/capsules

Creates a standalone capsule, pending until it starts. A capsule in a desk is made with POST /v1/desks/{id}/capsules. A standalone capsule never reaches an environment. The answer comes at once; call POST /v1/capsules/{id}/wait or poll GET /v1/capsules/{id} for the end. A fast main process can end without ever being running.

FieldTypeRequiredRule
teamstringnoA team you belong to. Absent, empty, or your own handle means personal. Case is ignored
imagestringnoDefault is the platform workspace image
packagesstring[]noNix package names, optionally pinned name@version; resolved before anything is written
from_workspacestringnoA workspace of yours (id, name, or team/name) in the same team; the capsule starts as a copy of its live files. Exclusive with from_snapshot
from_snapshotstringnoA reference: a volume name, name@N, or a snapshot id. The snapshot must be on a volume of the owner. The capsule starts from a copy of it. Exclusive with from_workspace
cmdstring[]yesThe main process: the program, then its arguments. At most 256 arguments and 256 KiB. The capsule ends when it exits. 422 when empty
ttybooleannoRun cmd on a pty. Default false
stdinbooleannoKeep the input open for attach. Default false
timeout_secsintegernoThe hard lifetime, 10 to 86400. Default 300
networkstringnointernet (default: DNS and the public internet, never the cluster) or none (nothing, DNS included)
envobjectnoVariables for the main process. At most 64; names are [A-Za-z_][A-Za-z0-9_]*; not HOME and not KD_…; a value is at most 32 KiB. A value of exactly ${{ secrets.NAME }} is a secret of the capsule's team (or yours, for a personal capsule)
cpuintegerno1 to 8. Default 1
memory_gbintegerno1 to 32. Default 2
json
{ "packages": ["python3"], "timeout_secs": 600, "network": "none", "env": { "MODE": "test" }, "cpu": 2, "memory_gb": 4 }

Response: 202, the capsule document.

A create that fails after the record is written leaves it terminated with reason Failed and the error as message.

Errors (every body is JSON):

  • 403 — your access to the team is paused.
  • 404 — no such team; from_workspace is not found or not yours; from_snapshot does not exist, has no such version (volume {name} has no version {n}), or is not on a volume of the owner
  • 409 — over quota (capsules, cpu, memoryGb, or diskGb). workspace … is not in the capsule's team, because a capsule copies only the workspaces of its own team. The source workspace has no volume yet (not ready yet: no volume for this workspace). A snapshot is already being cut for it. The storage pool is full (the storage pool is full; retry later or ask an operator). The team has no region.
  • 422 — create a desk's capsule with POST /v1/desks/{id}/capsules when the body has desk; timeout_secs must be between 10 and 86400; cpu must be between 1 and 8; memory_gb must be between 1 and 32; from_workspace and from_snapshot are exclusive; a from_snapshot with a bad version such as name@0; an env problem (env: at most 64 variables, env: "1A" is not a variable name, env: HOME is set by the platform, env: KEY is longer than 32 KiB); a package entry that is malformed, unknown, or not cached. A secret reference that does not exist is `DB_URL` is not a secret of acme.
  • 503 — the API cannot reach a package index to resolve a pin.

GET /v1/capsules

Your capsules, newest first, as an array of documents, ended ones included until the record is removed. ?team={slug} limits it to one team; your own handle means the personal ones.

The list comes in pages. ?limit= sets the page size: 50 by default, between 1 and 200. When more capsules remain, the response header X-Next-Cursor holds a cursor. Send it back as ?cursor= to read the next page. The header is absent on the last page. A cursor the list did not return is 422.

?state= keeps one state (waiting, running, ended, stopped, timed-out, failed) and ?q= keeps capsules whose id, image, packages or command contain the text; both apply before the page is cut.

Response: 200.

json
[  {    "id": "cap-3c4d5e6f7a8b9c0d",    "owner": "karthik",    "team": "",    "region": "centralindia",    "image": "ghcr.io/kloudlite/kloudlite-workspace",    "packages": [],    "phase": "terminated",    "reason": "Exited",    "message": null,    "created_at": "2026-10-18T09:30:00Z",    "started_at": "2026-10-18T09:30:12Z",    "finished_at": "2026-10-18T09:30:42Z",    "timeout_secs": 300,    "cmd": ["python3", "main.py"],    "tty": false,    "stdin": false,    "exit_code": 0,    "network": "internet",    "cpu": 1,    "memory_gb": 2,    "from": null,    "desk": null,    "snapshots": [],    "basedOn": null  }]

GET /v1/capsules/{id}

One capsule, by id only; a name does not resolve here.

Response: 200, the capsule document.

Errors:

  • 400 — invalid name (plain text): the id is not a valid path segment.
  • 404 — not found (plain text): no such capsule, or not yours.

POST /v1/capsules/{id}/terminate

Asks the platform to end the capsule. The record stays. Takes no body, and is safe to repeat.

Response: 202 with the document while the end is requested; 200 with the document when the capsule had already ended or its container was already gone.

json
{  "id": "cap-3c4d5e6f7a8b9c0d",  "owner": "karthik",  "team": "",  "region": "centralindia",  "image": "ghcr.io/kloudlite/kloudlite-workspace",  "packages": ["python3"],  "phase": "running",  "reason": null,  "message": null,  "created_at": "2026-10-18T09:30:00Z",  "started_at": "2026-10-18T09:30:12Z",  "finished_at": null,  "timeout_secs": 300,  "cmd": ["python3", "main.py"],  "tty": false,  "stdin": false,  "exit_code": null,  "network": "internet",  "cpu": 1,  "memory_gb": 2,  "from": null,  "desk": null,  "snapshots": [],  "basedOn": null}

The document is the one read before the request, so phase is not yet terminated; poll GET /v1/capsules/{id} for reason: "Terminated".

Errors:

  • 400 — invalid name (plain text).
  • 404 — not found (plain text).

DELETE /v1/capsules/{id}

Deletes the record. A capsule still running dies with it. Takes no body.

Response: 204 with an empty body.

Errors:

  • 400 — invalid name (plain text).
  • 404 — not found (plain text).

GET /v1/capsules/{id}/logs

Returns the main process's output as plain text. While the capsule runs it is the live output; after it ends it is the last 256 KiB, kept with the record for 90 days. With ?follow=true the response streams until the process ends.

The output shows *** in place of each secret value that the capsule can refer to. These are your personal secrets in the team and the secrets of the team. A value shorter than 4 bytes, or an encoded form of a value, is not masked.

Errors:

  • 404 — not found: no such capsule, or not yours.
  • 409 — capsule cap-… has no output yet (starting), while the capsule has not started. After it starts, the answer is 200 with an empty body until the first output.

GET /v1/capsules/{id}/attach

Opens a WebSocket to the main process's input and output. Frames are binary and byte 0 is the channel. The client sends 0 for input and 4 for a window size ({"cols":N,"rows":M}, pty only). The server sends 1 for output, 2 for error output (no pty only), and 3 for the exit ({"code":N}), then closes. The query takes cols and rows, 1 to 1000. Closing the socket detaches; it does not stop the process.

A capsule that ends by Terminated, Timeout, or any reason except Exited sends {"code":137,"reason":"Terminated"}. The code 137 is 128 plus the kill signal. The query takes cols and rows, 1 to 1000. Closing the socket detaches; it does not stop the process.

Errors:

  • 404 — not found: no such capsule, or not yours.
  • 409 — the capsule was created without stdin, is not running (capsule cap-… is pending; wait until it is running), or has ended (capsule cap-… has terminated (Exited)).
  • 426 — the request is not a WebSocket upgrade.
  • 502 — the capsule refused an attach.

POST /v1/capsules/{id}/wait

Holds until the capsule ends or timeout_ms passes, at most 20000, then returns {"id", "phase", "reason", "exit_code", "message"}. exit_code is null until the end, and after an end that was not the main process's. Call again while phase is not terminated.

Errors:

  • 404 — not found: no such capsule, or not yours.
  • 422 — the body is not a JSON object.