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.
{ "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}| Field | Type | Meaning |
|---|---|---|
id | string | sbx- and 16 hex digits |
owner, team | string | the owner's handle; the team slug, "" for a personal sandbox |
region | string | the region it runs in |
image, packages | string, string[] | what it was created with |
phase | string | pending, starting, running or terminated. A sandbox never stops and starts again |
reason | string or null | why 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 |
message | string or null | the detail behind reason |
created_at, started_at, finished_at | string or null | RFC 3339. started_at is the container's first ready moment; the timeouts count from it |
timeout_secs | integer | the lifetime limit |
cmd, tty, stdin | string[], boolean, boolean | the main process as created |
exit_code | integer or null | the main process's exit code, once it exited on its own |
network | string | internet or none |
cpu, memory_gb, disk_gb | integer | the size |
from | object or null | {"workspace": id} or {"snapshot": id} when it started as a copy |
session | string or null | the session that manages it |
snapshots | string[] | snapshots pushed from it; they outlive it |
basedOn | string or null | for 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.
| Field | Type | Required | Rule |
|---|---|---|---|
team | string | no | A team you belong to. Absent, empty or your own handle means personal. Case is ignored |
image | string | no | Default is the platform workspace image |
packages | string[] | no | Nix package names, optionally pinned name@version; resolved before anything is written |
from_workspace | string | no | A 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_snapshot | string | no | A 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 |
cmd | string[] | yes | The main process: the program, then its arguments. At most 256 arguments and 256 KiB. The sandbox ends when it exits. 422 when empty |
tty | boolean | no | Run cmd on a pty. Default false |
stdin | boolean | no | Keep the input open for attach. Default false |
timeout_secs | integer | no | The hard lifetime, 10 to 86400. Default 300 |
network | string | no | internet (default: DNS and the public internet, never the cluster) or none (nothing, DNS included) |
env | object | no | Environment 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) |
cpu | integer | no | 1 to 8. Default 1 |
memory_gb | integer | no | 1 to 32. Default 2 |
disk_gb | integer | no | Default 10; clamped to between 1 and the region's ceiling |
{ "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_workspaceis not found or not yours;from_snapshotdoes not exist, has no such version (volume {name} has no version {n}), or is not on a volume of the owner409— over quota (sandboxes,cpu,memoryGbordiskGb).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}/sandboxeswhen the body hassession;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; afrom_snapshotwith a bad version such asname@0; anenvproblem (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.
[ { "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.
{ "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 withoutstdin, 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.