Skip to content
DocsAll docs
Console

Sandboxes API

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

Last updated

On this page

The sandbox document

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

json
{  "id": "sbx-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,  "disk_gb": 10,  "from": null,  "session": null,  "snapshots": [],  "basedOn": null}
FieldTypeMeaning
idstringsbx- and 16 hex digits
owner, teamstringthe owner's handle; the team slug, "" for a personal sandbox
regionstringthe region it runs in
image, packagesstring, string[]what it was created with
phasestringpending, starting, running or terminated. A sandbox 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
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_gb, disk_gbintegerthe size
fromobject or null{"workspace": id} or {"snapshot": id} when it started as a copy
sessionstring or nullthe session 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/sandboxes

Creates a standalone sandbox, pending until it starts. A sandbox in a session is made with POST /v1/sessions/{id}/sandboxes. A standalone sandbox never reaches an environment. The answer comes at once; call POST /v1/sandboxes/{id}/wait or poll GET /v1/sandboxes/{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 sandbox 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 sandbox 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 sandbox 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)
envobjectnoEnvironment variables for the main process. At most 64; names are [A-Za-z_][A-Za-z0-9_]*; not HOME and not KL_…; a value is at most 32 KiB. A value of exactly ${{ secrets.NAME }} is a secret of the sandbox's team (or yours, for a personal sandbox)
cpuintegerno1 to 8. Default 1
memory_gbintegerno1 to 32. Default 2
disk_gbintegernoDefault 10; clamped to between 1 and the region's ceiling
json
{ "packages": ["python3"], "timeout_secs": 600, "network": "none", "env": { "MODE": "test" }, "cpu": 2, "memory_gb": 4 }

Response: 202, the sandbox 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 (sandboxes, cpu, memoryGb or diskGb). workspace … is not in the sandbox's team, because a sandbox 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, or its host is down with no sync point held elsewhere. The team has no region.
  • 422 — create a session's sandbox with POST /v1/sessions/{id}/sandboxes when the body has session; 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/sandboxes

Your sandboxes, 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 sandboxes 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.

Response: 200.

json
[  {    "id": "sbx-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,    "disk_gb": 10,    "from": null,    "session": null,    "snapshots": [],    "basedOn": null  }]

GET /v1/sandboxes/{id}

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

Response: 200, the sandbox document.

Errors:

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

POST /v1/sandboxes/{id}/terminate

Asks the platform to end the sandbox. 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 sandbox had already ended or its container was already gone.

json
{  "id": "sbx-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,  "disk_gb": 10,  "from": null,  "session": null,  "snapshots": [],  "basedOn": null}

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

Errors:

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

DELETE /v1/sandboxes/{id}

Deletes the record. A sandbox 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/sandboxes/{id}/logs

Returns the main process's output as plain text. While the sandbox 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.

Errors:

  • 404 — not found: no such sandbox, or not yours.
  • 409 — sandbox sbx-… has no output yet (starting).

GET /v1/sandboxes/{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 sandbox 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 sandbox, or not yours.
  • 409 — the sandbox was created without stdin, is not running (sandbox sbx-… is pending; wait until it is running) or has ended (sandbox sbx-… has terminated (Exited)).
  • 426 — the request is not a WebSocket upgrade.
  • 502 — the sandbox refused an attach.

POST /v1/sandboxes/{id}/wait

Holds until the sandbox 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 sandbox, or not yours.
  • 422 — the body is not a JSON object.