Sandboxes
| Route | What it does |
|---|---|
POST /sandboxes | Boot a sandbox. Body: name?, tags?, vcpu_count?, mem_size_mib?, image_id?, drives?, rate_limit? ({bandwidth_bytes_per_sec?, ops_per_sec?}, Firecracker’s own token-bucket limiter), egress? ({mode: "allow_all"|"deny_all", allow_cidrs?, deny_cidrs?} — see Networking & isolation). A request with no drives/rate_limit matching a configured pool’s image/resources transparently resumes a warm snapshot instead of cold-booting — see Startup latency & the pre-warmed pool; egress doesn’t disqualify a pool match, it’s applied fresh after either way. |
GET /sandboxes | List sandboxes. ?tag.<key>=<value> filters (repeatable, all must match). |
GET /sandboxes/history | Durable history of every sandbox that ever existed and how it ended (sqlite-backed, survives a daemon restart, unlike this list itself). ?live_only=true, ?limit=<n>. |
GET /sandboxes/by-name/:name | Resolve a name to a live sandbox’s id. 409 if the name currently belongs to a held snapshot instead. |
POST /sandboxes/get-or-create | Resolve-or-create by name in one call. Body: name (required), tags?, vcpu_count?, mem_size_mib?, image_id?, rate_limit?, drives?, egress? (the last five used only if a fresh sandbox is created — resuming an existing snapshot always uses what was recorded on it). |
DELETE /sandboxes/:id | Stop a sandbox. Preserves state as a snapshot by default (200, {"kept": true, "snapshot_id": "..."}); ?keep=false fully destroys instead (204). See Sandbox lifecycle. |
POST /sandboxes/:id/exec | Run a command. Body: {"command": "...", "args": [...]}. Returns stdout/stderr/exit_code. |
POST /sandboxes/:id/read-file | Body: {"path": "..."}. Returns {"content_base64": "..."}. |
POST /sandboxes/:id/write-file | Body: {"path": "...", "content_base64": "..."}. |
POST /sandboxes/:id/chmod | Body: {"path": "...", "mode": <octal-as-int>}. |
POST /sandboxes/:id/chown | Body: {"path": "...", "uid": <n>, "gid": <n>}. |
POST /sandboxes/:id/mkdir | Body: {"path": "...", "parents"?: <bool>}. |
POST /sandboxes/:id/rename | Body: {"from": "...", "to": "..."}. |
POST /sandboxes/:id/copy | Body: {"from": "...", "to": "..."}. |
POST /sandboxes/:id/symlink | Body: {"target": "...", "link_path": "..."}. |
POST /sandboxes/:id/readlink | Body: {"path": "..."}. Returns {"target": "..."}. |
POST /sandboxes/:id/truncate | Body: {"path": "...", "size": <n>}. |
POST /sandboxes/:id/list-dir | Body: {"path": "..."}. Returns real per-entry metadata: type, permission bits, size, mtime. |
GET /sandboxes/:id/pty[?cols=&rows=] | Upgrades to a WebSocket: a live, bidirectional shell session, distinct from exec’s request/response shape. Accepts the token as ?token= as well as a header, since neither browsers’ nor Node’s native WebSocket constructor can set a custom header. A per-sandbox concurrent-session cap (64) is enforced. |
POST /sandboxes/:id/exec-stream | Starts a command in the background, returns {"id": "..."} immediately — the command itself may not even be spawned yet. Attach to the logs route below to actually see its output. Not exec’s request/response shape: this is for a long-running command you want to watch live, not wait on. |
GET /sandboxes/:id/exec-stream | Lists every streamed session tracked against this sandbox, running or finished: {"sessions": [{"id", "command", "args", "started_at_unix", "exit_code"}, ...]} — exit_code is null while still running. |
GET /sandboxes/:id/exec-stream/:session_id/logs | Upgrades to a WebSocket: replays everything captured so far, then live-tails anything new, until the process exits (one final "[process exited with code N]" text message, then the socket closes) or the client disconnects. Callable again any number of times for the same session, including long after it finished — it’s just a replay with nothing to tail. Buffered replay frames arrive as binary; the final exit notice arrives as text — a client needs to handle both frame types. Sessions aren’t carried across resume()/fork() and don’t survive a daemon restart. |
Snapshots
| Route | What it does |
|---|---|
POST /sandboxes/:id/snapshot | Pause, snapshot, stop. Returns a snapshot id. |
GET /snapshots | List snapshots. ?source_sandbox_id=<id> narrows to the one taken from that sandbox; ?parent_snapshot_id=<id> narrows to whatever was forked/resumed from that snapshot (can be more than one, over time). Each result includes parent_snapshot_id — see Snapshots, resume, and fork. |
POST /snapshots/:id/resume | Boot a new sandbox from the snapshot, consuming the record. Retires the underlying checkpoint into GET /snapshots/history by default rather than deleting it — see Time-travel restore. ?retain_history=false opts back into the original, fully-destructive behavior. |
POST /snapshots/:id/fork | Boot a new sandbox from the snapshot without consuming it. 409 while an earlier fork is still live. |
DELETE /snapshots/:id | Delete a snapshot outright. 409 while a fork of it is still live. |
Snapshot history (time-travel restore)
| Route | What it does |
|---|---|
GET /snapshots/history | List retired checkpoints — anything a resume retained instead of deleting. Same ?source_sandbox_id=/?parent_snapshot_id= filters as GET /snapshots. |
POST /snapshots/history/:id/restore | Boot a new sandbox from a retired checkpoint. Does not consume it — restorable again later. 409 (naming the current holder) while anything live or held shares that checkpoint’s frozen network identity. |
DELETE /snapshots/history/:id | Delete a retired checkpoint outright, reclaiming its disk usage (a full guest-memory dump plus a private rootfs copy) — there’s no automatic expiry yet. |
Preview
| Route | What it does |
|---|---|
GET/POST/... /sandboxes/:id/preview/:port[/*path] | Reverse-proxies a full HTTP request to that port inside the sandbox. Accepts the token as ?token= as well as a header. |
Local tunnel
The reverse of preview: exposes a service running on the caller’s machine to code inside the sandbox. See Local tunnel and local tunnel: the mechanism.| Route | What it does |
|---|---|
POST /sandboxes/:id/tunnel | Body: {"guest_port": <n>}. Tells the guest agent to start listening on guest_port. Returns {"tunnel_id": "...", "guest_port": <n>}. |
GET /sandboxes/:id/tunnel/:tunnel_id/ws | Upgrades to a WebSocket: the long-lived relay the caller holds open for the tunnel’s whole life, multiplexing every guest-accepted connection over it with a small binary frame. Accepts the token as ?token= as well as a header, same reason as /pty above. |
DELETE /sandboxes/:id/tunnel/:tunnel_id | Tells the guest agent to stop listening and tears down the relay. |
Example
Writing a file into a sandbox, then reading it back — the HTTP API overview already covers create/exec/stop, so this shows the other half of the Sandboxes surface instead:curl -si -X POST http://127.0.0.1:7777/sandboxes/12b9dd16-5787-44fa-aceb-5c9bb404b854/write-file \
-H 'content-type: application/json' \
-d '{"path":"/tmp/config.json","content_base64":"eyJvayI6dHJ1ZX0="}'
HTTP/1.1 204 No Content
curl -s -X POST http://127.0.0.1:7777/sandboxes/12b9dd16-5787-44fa-aceb-5c9bb404b854/read-file \
-H 'content-type: application/json' \
-d '{"path":"/tmp/config.json"}'
{"content_base64":"eyJvayI6dHJ1ZX0="}
resume consumes the snapshot, so a second resume of the same id 404s:
curl -s -X POST http://127.0.0.1:7777/sandboxes/12b9dd16-5787-44fa-aceb-5c9bb404b854/snapshot
{"snapshot_id":"a1e9c3d2-2f0a-4b1e-9c3a-7d5e6f8b9c0d"}
curl -s -X POST http://127.0.0.1:7777/snapshots/a1e9c3d2-2f0a-4b1e-9c3a-7d5e6f8b9c0d/resume
{"id":"f3b2a1c0-9e8d-4c7b-a6f5-e4d3c2b1a0f9"}
curl -si -X POST http://127.0.0.1:7777/snapshots/a1e9c3d2-2f0a-4b1e-9c3a-7d5e6f8b9c0d/resume
HTTP/1.1 404 Not Found
content-type: application/json
{"error":"sandbox not found: a1e9c3d2-2f0a-4b1e-9c3a-7d5e6f8b9c0d"}