SB-Box Documentation
Build a template once, start isolated sandboxes from it in under a second, run commands and move files — from the console, the gsk CLI or the REST API.
SB-Box gives your code and your agents an isolated Linux machine on demand. Each sandbox is a full virtual machine with its own kernel, restored from a template snapshot in under half a second, frozen when it goes quiet (only its disk keeps billing, at the disk rate) and resumed with its memory intact.
Three building blocks:
| What it is | Where it is explained | |
|---|---|---|
| Template | A booted, snapshotted environment: an image plus what should already be running. Build it once. | §13 Templates |
| Sandbox | A running machine restored from a template. Run commands, move files, expose ports. | §3 Sandbox lifecycle |
| Freeze & versions | A frozen sandbox keeps disk and memory; only its disk bills. Every freeze is a save point you can fork or roll back to. | §4 Persistence, §5 Versions |
Three surfaces, same objects:
| Surface | Best for |
|---|---|
| Console (this site: Templates, Sandboxes, Usage) | building templates, browsing sandboxes, an in-browser shell, usage |
gsk sb-box CLI |
scripts, agents and quick manual work from any machine |
REST API https://www.genspark.ai/api/sb-box/v1 (beta, §15) |
your own integrations |
The responses shown are real (ids and timestamps shortened).
1. Quickstart
1.1 Running your first sandbox
1. Sign in to Genspark and open the SB-Box console.
2. Get your API key. Install the CLI and log in once; the saved
gsk-... key serves both the CLI and the REST API.
npm install -g @genspark/cli
gsk login # browser login; key saved to ~/.genspark-tool-cli/config.json
gsk login-info # shows the account, plan and credit balance
3. Pick a template — or don't. gsk sb-box create with no --template
starts from the newest public base (Ubuntu 24.04, Python 3 with numpy /
pandas / matplotlib, Node 22, git). REST has no default: it needs
templateName or templateId.
gsk sb-box templates
# → {"templates": [{"id": "tpl-7b7c…", "name": "sb-box-base-<sha>-<date>",
# "namespace": "public", "status": "ready", "can_create": true, …}, …]}
4. Create a sandbox, run a command, kill it.
gsk sb-box create --timeout 600 # public base; --template <name|tpl-id> for another
# → {"id": "sbx-9eb7…", "state": "running", "timeoutAt": "…T11:47:08Z",
# "cpuCount": 2, "memoryMB": 4096, "diskSizeGB": 20, "idleTimeout": 600, "region": "westus2",
# "portUrl": "https://{port}-sbx-9eb7….sandbox.westus2-prod.sspark.ai", "domain": "sandbox.westus2-prod.sspark.ai"}
gsk sb-box exec --sandbox_id sbx-9eb7… --command 'echo hello from gsk'
# → {"stdout": "hello from gsk\n", "stderr": "", "exitCode": 0}
gsk sb-box kill --sandbox_id sbx-9eb7…
# → {"status": "killed", "sandbox_id": "sbx-9eb7…"}
The CLI prints one JSON envelope per call: {"version": 1, "status": "ok", "message": "success", "data": …}. In scripts, take .data:
ID=$(gsk sb-box create --timeout 600 | jq -r .data.id)
The same four steps over REST:
API=https://www.genspark.ai/api/sb-box/v1
H='X-Api-Key: gsk-...'
curl -s "$API/templates" -H "$H" | jq '.templates[] | select(.status=="ready") | .name'
# "base" always means the newest ready public base (its catalog names are dated and roll)
SBX=$(curl -s -X POST "$API/sandboxes" -H "$H" -H 'Content-Type: application/json' \
-d '{"templateName": "base", "timeout": 600}' | jq -r .id)
curl -s -X POST "$API/sandboxes/$SBX/exec" -H "$H" -H 'Content-Type: application/json' \
-d '{"command": ["/bin/sh", "-c", "echo hello; uname -r"]}'
# → {"stdout":"hello\n6.18.15\n","stderr":"","exitCode":0}
curl -s -X DELETE "$API/sandboxes/$SBX" -H "$H"
# → {"status":"killed"}
1.2 Upload and download files
Files move in and out by path. The default user is user and its home is
/home/user; relative paths resolve there. Parent directories are created.
gsk sb-box write --sandbox_id $ID --path /home/user/data/in.txt --content 'hello from the guide'
# → {"written": 20, "path": "/home/user/data/in.txt"}
gsk sb-box read --sandbox_id $ID --path /home/user/data/in.txt
# → {"content": "hello from the guide", "path": "/home/user/data/in.txt"}
gsk sb-box ls --sandbox_id $ID --path /home/user
# → [{"name": "data", "path": "/home/user/data", "isDir": true, "size": 4096}, …]
Binary files go through REST (the body is the file, byte for byte):
curl -s -X POST "$API/sandboxes/$SBX/files/stream?path=/home/user/model.bin" -H "$H" \
--data-binary @model.bin # → {"written":3000}
curl -s "$API/sandboxes/$SBX/files/stream?path=/home/user/out.png" -H "$H" -o out.png
Upload a local text file with the CLI: --content "$(cat local.txt)". If the
content has $, quotes or newlines the shell would mangle, put the parameters
in a JSON file and pass --args-file (§14).
1.3 Install custom packages
Two ways:
-
At runtime, in the sandbox you already have. Fast to try, gone when the sandbox is killed, and every new sandbox pays the install again.
gsk sb-box exec --sandbox_id $ID --command 'pip install -q requests && python3 -c "import requests; print(requests.__version__)"' -
In a template, once. Every sandbox created from it starts with the packages installed and the runtime warm — use this for anything you will start more than a few times (§13).
gsk sb-box build-template --template py-analysis --image python:3.12-slim \ --cpu_count 2 --memory_mb 2048 \ --start_cmd 'pip install -q pandas matplotlib && touch /tmp/ready && sleep infinity' \ --ready_cmd 'test -f /tmp/ready' # → {"templateId": "tpl-…", "status": "building", "region": "westus2"} gsk sb-box templates # … py-analysis ready (a few minutes later) gsk sb-box create --template py-analysis --timeout 600
2. API key and authentication
One key drives the CLI and the REST API. Send it in either header:
X-Api-Key: gsk-...
Authorization: Bearer gsk-...
Keep it secret: it acts as you. gsk login stores it in
~/.genspark-tool-cli/config.json; GSK_API_KEY in the environment overrides
the file. A call without a valid key answers 401 Invalid or expired API token.
An account without SB-Box access gets 403 {"detail": "SB-Box is not enabled for this account"} on every call.
SB-Box is available on paid plans (Plus, Pro, Team, Enterprise). A Free
account gets 403 {"detail": {"code": "sb_box_plan_required", "plan": "free", "message": "…", "upgrade": "https://www.genspark.ai/pricing"}} on every
call that creates or runs something (create, resume, clone, reset, build,
commands, files). Reads, freeze, kill and template delete still work
(billing §2).
3. Sandbox lifecycle
A sandbox is created from a template, runs until its deadline or until it idles, is frozen (disk and memory kept; only the disk bills), resumes any number of times, and ends when you kill it.
3.0 States and transitions
Solid boxes are live states; the dashed box is terminal. Arrows are calls you make or platform timers.
state |
What it means | Machine | Billed | Leaves via |
|---|---|---|---|---|
starting |
Snapshot being restored (a create, cold resume, rollback or reset); well under a second. | allocating | no | → running, or failed if the restore cannot complete |
running |
Booted guest: commands, files, ports and the interpreter work. | held | yes | freeze / idle timer / onTimeout: freeze → freezing; guest init exits → exited; kill / onTimeout: kill → killed |
freezing |
Memory and disk are being saved; usually a few seconds. | releasing | no | → frozen |
frozen |
Snapshot on storage, machine released, the previous snapshot archived as a version. Resume takes about a second. Memory snapshot kept 14 days untouched (§4.1). |
none | disk only | resume, any exec / file / terminal call, or an HTTP request to a port URL → starting → running; rollback → starting; clone leaves it untouched; kill → killed |
exited |
The guest's init process ended on its own. The snapshot is preserved. | none | disk only | resume → starting → running (a fresh init); rollback; kill |
failed |
The sandbox could not be restored or run. | none | no | reset or rollback → starting; kill |
killed |
Terminal: kill, the lifetime deadline with onTimeout: kill, or a sandbox that could not be kept running. Your own kill makes the id 404; a kill by timeout or failure leaves the record readable as killed for a while: GET (sandbox, grants) and kill (no-op 200) answer, acting calls answer 409, reset / rollback 410. |
none | no | — |
unknown |
Only in list rows: the sandbox's region could not be read just now. |
— | — | — |
Rules of thumb:
- Only
runningbills compute;frozenandexitedbill the disk; every other state bills nothing (billing §1). Frozen sandboxes do not count against the concurrency cap either. - Three things freeze: an explicit
freeze, the idle timer (10 quiet minutes), and the lifetime deadline withonTimeout: freeze(the default; a deadline kills only withonTimeout: kill, REST only) (§3.2). - Every freeze is a save point: the snapshot it replaces is archived as
the next
version(the first freeze files the template baseline as v1), sofrozenis whereclone --versionandrollbackoperate (§5). frozenresumes itself: you rarely callresume— the firstexec, file, terminal or port request wakes the machine (about 4 s end to end for that first call) and the state passes throughstartingtorunning(§4).killedis final: disk and versions are gone; there is no un-kill.
3.1 Create
gsk sb-box create --timeout 1800 # public base; --template <name|tpl-id> for another
curl -s -X POST "$API/sandboxes" -H "$H" -H 'Content-Type: application/json' -d '{
"templateName": "base", # or your own template's name, or "templateId": "tpl-…" (one required)
"timeout": 1800, # seconds; omit = no deadline (the idle timer still applies)
"lifecycle": {"onTimeout": "freeze"}, # freeze (default) | kill
"metadata": {"purpose": "docs", "userId": "u-123"},
"network": {"allowInternetAccess": true} # see §10
}'
| Field | Type · default | Meaning |
|---|---|---|
templateId |
string | Exact template id (tpl-…). One of templateId / templateName is required (400 otherwise). |
templateName |
string | Template name; resolves to the newest ready template of that name you can see (your own, shared with you, or the public catalog). base is an alias for the newest ready public base (sb-box-base-<sha>-<date>). Wins over templateId when both are sent. |
templateNamespace |
string · omit | Restrict the name lookup to one catalog: public for the shared catalog, or omit to search everything you can see. Any other value answers 400. |
timeout |
number (seconds) · none | Lifetime. Omitted or 0 = no deadline. The older timeoutMs (milliseconds) still works but answers with Deprecation: true and a Warning: 299 header. |
lifecycle.onTimeout |
"freeze" | "kill" · freeze |
What happens at the deadline. gsk sb-box create always sends kill. |
metadata |
object of strings · {} |
Free-form key/values, echoed back on every read (§6). |
envVars |
object of strings · {} |
Sandbox-wide environment: exported into every command the sandbox runs; per-command envVars layer on top (§7). |
network |
object · omit | Egress and public-URL policy: allowInternetAccess, allowOut, denyOut, allowPublicTraffic, maskRequestHost (§10). |
namespace |
string · omit | A grouping label of your choosing (letters, digits, _ - .). Stored as <your user id>/<label> on the record and usable as the namespace filter of GET /sandboxes. Without it each sandbox gets its own generated label. |
Response:
{"id": "sbx-44fb…", "templateId": "tpl-7b7c…", "state": "running", "region": "westus2",
"portUrl": "https://{port}-sbx-44fb….sandbox.westus2-prod.sspark.ai", "domain": "sandbox.westus2-prod.sspark.ai",
"createdAt": "2026-09-21T11:32:30Z", "startedAt": "2026-09-21T11:32:31Z",
"timeoutAt": "2026-09-21T11:47:30Z",
"cpuCount": 2, "memoryMB": 4096, "diskSizeGB": 20,
"idleTimeout": 600,
"metadata": {"purpose": "docs", "userId": "u-123", "cogenId": "…"}}
A create takes about half a second over REST (a few seconds via the CLI). Parallel creates, resumes and clones of one account are serialised; one may wait up to 10 s.
3.2 Timeouts: the lifetime and the idle timer
Two independent timers:
| Timer | Set by | Default | What happens |
|---|---|---|---|
| Lifetime | timeout on create; POST /sandboxes/{id}/timeout later |
none (REST and gsk create) |
at timeoutAt the sandbox is frozen (onTimeout: freeze, the default) or killed (kill, REST only). |
| Idle | the platform | 600 s | no guest activity (network traffic, CPU) for this long → automatic freeze, landing up to 5 minutes later than the timeout — expect a quiet sandbox to be frozen 10 to 15 minutes after its last activity. Reported as idleTimeout on every read; not a request field. |
Change the lifetime while the sandbox runs. The value is seconds from now:
gsk sb-box timeout --sandbox_id $ID --timeout 1800 # → {"status": "ok"}
curl -s -X POST "$API/sandboxes/$SBX/timeout" -H "$H" -H 'Content-Type: application/json' -d '{"timeout": 1800}'
GET /sandboxes/{id} then shows the new timeoutAt. Resuming or rolling
back with {"timeout": 600} also re-grants a lifetime from now
(§4).
Clear an existing deadline with {"timeout": 0}, {} or an empty body —
the sandbox then runs until the idle timer freezes it.
3.3 Get information
gsk sb-box get --sandbox_id $ID
curl -s "$API/sandboxes/$SBX" -H "$H"
Fields:
| Field | Meaning |
|---|---|
id |
The sandbox id (sbx-…); what every other call takes. |
templateId |
The template it was restored from. On a fork this reads <source id>-clone or <source id>-clone-v<N>. |
state |
Live state (§3.0). |
region |
The region it runs in (also present as region on templates). |
portUrl, domain |
Where its ports are reachable: portUrl is the public URL with a literal {port} placeholder (https://{port}-sbx-….sandbox.westus2-prod.sspark.ai), domain the host suffix (sandbox.westus2-prod.sspark.ai) for <port>-<id>.<domain> construction (§10.1). |
createdAt, startedAt |
When it was created; when the current run started (a resume resets startedAt). The create / resume response shows sub-second precision; a later GET shows whole seconds. |
frozenAt |
When the current save point was frozen (the state a resume or rollback restores); set by every freeze, kept while running. |
timeoutAt |
The lifetime deadline; absent when there is none. |
cpuCount, memoryMB, diskSizeGB |
Provisioned size (what is billed, billing §1). |
idleTimeout |
The idle-freeze timeout in force, seconds (platform-set, 600). |
versions[] |
Archived freeze points {version, frozenAt, label}; present once the sandbox has been frozen (§5). |
metadata |
Your key/values plus cogenId (your account id) and, on a fork, clonedFrom (plus clonedVersion when forked from an archived version). |
trafficAccessToken |
Only on the create response, only when network.allowPublicTraffic is false (§10.1). |
source, namespace, is_owner, has_write, cogen_id, template_id, created_at, updated_at |
Also returned by GET /sandboxes/{id}: the namespace label, whether you own it or hold a write grant, and snake_case copies of the ids and timestamps. |
3.4 List
gsk sb-box list
curl -s "$API/sandboxes?state=running,frozen" -H "$H"
# → {"sandboxes": [{"id": "sbx-…", "template_id": "tpl-…", "state": "running", "region": "westus2",
# "portUrl": "https://{port}-sbx-….sandbox.westus2-prod.sspark.ai", "domain": "sandbox.westus2-prod.sspark.ai",
# "source": "user", "created_at": "…", …}], "hasMore": false}
Lists what you created, newest first. Rows are compact; GET /sandboxes/{id} has the full record. No pagination yet (hasMore is always
false). Sandboxes shared with you: GET /sandboxes/shared.
| Query | Meaning |
|---|---|
state |
Comma-separated list of states to keep: running, starting, freezing, frozen, exited, killed. Omit for all. killed rows are sandboxes killed by a timeout or a failure; your own kills leave the list at once. |
namespace |
Only sandboxes created with that namespace label — pass the label you chose (ctest) or its stored form <your user id>/ctest; both match. |
3.5 Kill
gsk sb-box kill --sandbox_id $ID # → {"status": "killed", "sandbox_id": "sbx-…"}
curl -s -X DELETE "$API/sandboxes/$SBX" -H "$H" # → {"status":"killed"}
Irreversible and owner-only. Afterwards the id answers 404 {"detail": "sandbox not found"}.
4. Persistence: freeze and resume
A freeze snapshots memory and disk to storage and releases the machine: files, running processes, open sockets and shell history all survive. While frozen only the disk bills (billing §1) and the sandbox does not count against your concurrency cap. The memory snapshot is kept for 14 days of inactivity (§4.1); the disk is kept, and billed, until you kill the sandbox.
gsk sb-box freeze --sandbox_id $ID
# → {"id": "sbx-…", "state": "frozen", "frozenAt": "…", "versions": [{"version": 1, …}]} # a few seconds
curl -s -X POST "$API/sandboxes/$SBX/freeze" -H "$H"
# → 200 the sandbox record with "state": "frozen" the common case
# 202 {"id":"sbx-…","state":"freezing","status":"freeze in progress"} only if it takes longer than ~50 s
# 200 {"id":"sbx-…","state":"frozen","status":"already frozen"} when it already was
On a 202 poll get until frozen before you fork or roll back. Repeating
the call during a freeze is harmless.
Resume brings it back, processes and memory intact, in about a second:
gsk sb-box resume --sandbox_id $ID
curl -s -X POST "$API/sandboxes/$SBX/resume" -H "$H" -H 'Content-Type: application/json' -d '{"timeout": 600}'
# → the sandbox record, "state": "running", "timeoutAt" re-granted 600 s from now
Resume is idempotent: on a running sandbox it answers 200 and changes
nothing (the optional timeout still re-grants the lifetime).
You rarely need to resume by hand. exec, files, background commands,
the terminal and HTTP requests to a port URL (§10) all
resume a frozen sandbox first (about 4 s end to end for that first call).
Besides freeze (the console's Freeze button is the same call), the idle
timer and onTimeout: freeze freeze a sandbox — and so does a zero credit
balance (billing §1).
4.1 Memory snapshots expire after 14 days frozen
A frozen sandbox holds its disk (files) for as long as it exists and its
memory snapshot (processes, sockets, interpreter variables, shell
history) for 14 days from the freeze. Any touch — resume, exec, a file
call, a port-URL request — restarts the clock at the next freeze.
What changes once the memory is reclaimed:
| State | Still frozen; the record gains metadata.memoryExpired: "true". |
| Disk | Intact, every file as it was at the freeze. It keeps billing at the disk rate until you kill the sandbox. |
| Memory | Gone: processes, open connections, interpreter state, environment of the running shell. Archived freeze points (versions) are cleared too — there is nothing to roll back to. |
| Refused calls | resume — explicit, or the automatic one behind exec, files, the terminal and port URLs — answers 428 with {"reason": "memory_expired", "recovery": ["reset"]} (top level, not under detail). rollback, clone and snapshot-template answer 428 as well. |
| Recovery | POST /sandboxes/{id}/reset (§15.3; REST only, the CLI has no reset verb): a cold boot from the disk — the template's start command runs again, files are where you left them, processes and memory state start from zero. The sandbox is running afterwards; its next freeze clears the flag. Reset is owner-only (a grantee's call answers 404, §12) — a write grantee must ask the owner. |
curl -s -X POST "$API/sandboxes/$SBX/exec" -H "$H" -H 'Content-Type: application/json' -d '{"command": ["ls"]}'
# → 428 {"reason": "memory_expired", "recovery": ["reset"]}
curl -s -X POST "$API/sandboxes/$SBX/reset" -H "$H"
# → the sandbox record, "state": "running"; files intact, processes restarted
To keep running state longer than two weeks untouched, resume the sandbox
(any exec will do) before the window closes, or keep the state you care
about on disk.
5. Versions: fork and roll back
Every freeze archives the snapshot it replaces as a version — memory,
processes and disk, not just files. A sandbox keeps its newest 5 versions;
older ones are deleted, and rollback / clone to a pruned version answers
404 — after six freezes, version 1 (the template baseline) is gone.
gsk sb-box versions --sandbox_id $ID
# → {"sandbox_id": "sbx-…", "state": "frozen",
# "versions": [{"version": 1, "frozenAt": "…T11:33:10Z", "label": "auto"},
# {"version": 2, "frozenAt": "…T11:34:44Z", "label": "auto"}]}
The first freeze archives the template baseline as version 1; the state
you froze first becomes version 2 at the next freeze, and so on.
GET /sandboxes/{id} carries the same list as versions[].
5.1 Fork
FORK=$(gsk sb-box clone --sandbox_id $ID --version 2 | jq -r .data.id)
gsk sb-box exec --sandbox_id $FORK --command 'cat /home/user/marker.txt' # the file as it was at v2
curl -s -X POST "$API/sandboxes/$SBX/clone" -H "$H" -H 'Content-Type: application/json' \
-d '{"version": 2, "metadata": {"purpose": "try-the-migration"}}'
# → {"id": "sbx-6d4a…", "state": "running", "timeoutAt": "<24 h from now>",
# "metadata": {"clonedFrom": "sbx-44fb…", "clonedVersion": "2", "purpose": "try-the-migration", …}}
A fork is a new sandbox booted from that save point, memory included; the
source is untouched and can be in any state. The fork starts running with a
24-hour deadline (adjust with /timeout); its metadata records
clonedFrom, plus clonedVersion when taken from an archived version.
clone with no version forks the live state of a running source (its
memory is snapshotted and the source resumes) or the active snapshot of a
frozen one.
5.2 Roll back
gsk sb-box freeze --sandbox_id $ID # rollback needs a settled sandbox
gsk sb-box rollback --sandbox_id $ID --version 2
# → the sandbox record, "state": "running": the machine is back at v2
gsk sb-box versions --sandbox_id $ID # the state you just left was archived first
curl -s -X POST "$API/sandboxes/$SBX/rollback" -H "$H" -H 'Content-Type: application/json' \
-d '{"version": 2, "timeout": 1800}' # version 0 = the last committed freeze; timeout optional
The sandbox returns to freeze point N and resumes; the state being left is
archived first, so a rollback is itself reversible. On a running sandbox it
answers
409 {"detail": "rollback requires a settled snapshot state (frozen/failed/exited), sandbox \"sbx-…\" is running"}
6. Metadata
Attach string key/values at create time; they come back on every read.
curl -s -X POST "$API/sandboxes" -H "$H" -H 'Content-Type: application/json' \
-d '{"templateId": "tpl-7b7c…", "timeout": 600, "metadata": {"userId": "u-123", "purpose": "docs"}}'
# → … "metadata": {"userId": "u-123", "purpose": "docs", "cogenId": "…"}
Added automatically: cogenId (your account id); clonedFrom and
clonedVersion on a fork. Metadata is not a list filter. clone accepts its
own metadata for the fork. The CLI does not expose metadata; use REST.
7. Environment variables
Pass variables per command; they are visible to that command only:
curl -s -X POST "$API/sandboxes/$SBX/exec" -H "$H" -H 'Content-Type: application/json' \
-d '{"command": ["/bin/sh", "-c", "echo $MODE"], "envVars": {"MODE": "demo"}}'
# → {"stdout":"demo\n","stderr":"","exitCode":0}
The same envVars field exists on background commands and the terminal
(§8). A plain exec sees the guest's own environment
(HOME=/home/user, PATH=…, PWD=/home/user).
envVars on create is the sandbox-wide environment: every exec,
background command and terminal sees it, a fork inherits it, and per-command
envVars layer on top. Settings every sandbox needs belong in the template
(a Dockerfile ENV, or a profile file the start command writes).
8. Commands
8.1 Run a command
gsk sb-box exec --sandbox_id $ID --command 'python3 -c "print(6*7)"'
# → {"stdout": "42\n", "stderr": "", "exitCode": 0}
curl -s -X POST "$API/sandboxes/$SBX/exec" -H "$H" -H 'Content-Type: application/json' -d '{
"command": ["/bin/bash", "-lc", "cd /home/user && python3 main.py"],
"cwd": "/home/user",
"envVars": {"DEBUG": "1"},
"user": "root",
"timeout": 120
}'
# → {"stdout":"…","stderr":"…","exitCode":0}
| Field | Meaning |
|---|---|
command |
An argv array; a bare string is rejected (400 command must be a non-empty list). Use ["/bin/sh", "-c", "…"] for shell syntax. The CLI's --command is wrapped in /bin/sh -c for you. |
cwd |
Working directory; default the user's home. |
envVars |
Variables for this command (§7). |
user |
Omit for the image's default user (user, uid 1000) or root. |
timeout |
Seconds; default 60 (CLI: 30), capped at 600. |
A non-zero exit is a normal 200:
{"stdout":"out\n","stderr":"err\n","exitCode":3}
Output is capped at 10 MiB per stream. A command that runs past timeout is
killed and the call answers 408 with "killed": true and whatever output
arrived (§15.4). Use a background command for anything long.
8.2 Stream output
POST /sandboxes/{id}/sse/exec takes the same body as exec and answers with Server-Sent Events:
curl -N -X POST "$API/sandboxes/$SBX/sse/exec" -H "$H" -H 'Content-Type: application/json' \
-d '{"command": ["/bin/sh", "-c", "echo one; sleep 1; echo two >&2; exit 2"]}'
event: execId
data: {"execId":"gp-stream-1789990365519283415"}
id: 1
event: stdout
data: {"data":"one\n"}
id: 2
event: stderr
data: {"data":"two\n"}
event: exit
data: {"exitCode":2}
8.3 Background commands
Start a process and come back for it later. A client disconnect never kills a
background command; only kill (or the sandbox ending) does.
# start → execId + pid
curl -s -X POST "$API/sandboxes/$SBX/commands/start" -H "$H" -H 'Content-Type: application/json' \
-d '{"command": ["python3", "-m", "http.server", "8000", "--directory", "/home/user"], "cwd": "/home/user", "envVars": {"A": "1"}}'
# → {"execId":"bg-1789990375798611452","pid":75}
curl -s "$API/sandboxes/$SBX/commands" -H "$H" # list
# → [{"execId":"bg-1789990375798611452","pid":75,"sandboxId":"sbx-…"}]
curl -s -X POST "$API/sandboxes/$SBX/commands/$EXEC/input" -H "$H" -H 'Content-Type: application/json' \
-d '{"data": "y\n"}' # → {"written":2}
curl -s -X POST "$API/sandboxes/$SBX/commands/$EXEC/close" -H "$H" # stdin EOF → {"status":"ok"}
curl -s -X POST "$API/sandboxes/$SBX/commands/$EXEC/wait" -H "$H" # blocks → {"exitCode":0}
curl -s -X POST "$API/sandboxes/$SBX/commands/kill" -H "$H" -H 'Content-Type: application/json' \
-d '{"pid": 75, "signal": 15}' # → {"status":"ok"}
| Call | Body | Meaning |
|---|---|---|
commands/start |
command[] (argv, required), cwd, envVars, user |
start the process; returns execId (for input / close / wait) and pid (for kill / send-stdin) |
commands (GET) |
— | processes started in this sandbox that have not been waited on yet |
commands/{execId}/input |
data (string) |
write to the process's stdin; returns {"written": <bytes>} |
commands/{execId}/close |
— | close stdin (EOF) |
commands/{execId}/wait |
— | block until the process exits; returns {"exitCode": n} |
commands/kill |
pid (required), signal (number, default 9) |
send a signal; 15 for a graceful stop |
commands/send-stdin |
pid, data |
write to stdin by pid instead of execId |
An entry stays in the list until you wait on it, even after the process has
exited or been killed. Output is not streamed back from a background command:
redirect it to a file and read the file.
8.4 Interactive terminal (PTY)
The console's Shell tab is a PTY. Over REST:
curl -s -X POST "$API/sandboxes/$SBX/pty" -H "$H" -H 'Content-Type: application/json' \
-d '{"command": ["/bin/bash"], "rows": 24, "cols": 80, "cwd": "/home/user", "envVars": {"PS1": "$ "}}'
# → {"execId":"pty-1789990381393509857","pid":81}
curl -s -X POST "$API/sandboxes/$SBX/pty/$PTY/input" -H "$H" --data-binary $'echo pty-works\n' # raw bytes
curl -N "$API/sandboxes/$SBX/pty/$PTY/connect" -H "$H" # SSE: event: data / data: {"data": "…"}
curl -s -X POST "$API/sandboxes/$SBX/pty/$PTY/resize" -H "$H" -H 'Content-Type: application/json' -d '{"rows": 40, "cols": 120}'
curl -s -X POST "$API/sandboxes/$SBX/pty/$PTY/kill" -H "$H"
| Call | Body | Meaning |
|---|---|---|
pty (POST) |
command[] (default ["/bin/bash"]), rows (24), cols (80), cwd, envVars |
open a terminal; returns execId and pid |
pty/{execId}/input |
raw bytes | keystrokes, including the trailing \n for Enter |
pty/{execId}/connect (GET) |
— | Server-Sent Events, event: data / data: {"data": "…"} with the terminal bytes; ends with event: exit carrying exitCode |
pty/{execId}/resize |
rows, cols |
in characters |
pty/{execId}/kill |
— | end the session |
ws/pty (WebSocket) |
?api_key=gsk-… (or the X-Api-Key header) |
both directions on one connection, for a real terminal client |
The session runs with TERM=xterm-256color; colours and escape sequences work.
8.5 Run code on the stateful interpreter
Every sandbox created from a public base dated 2026-10-13 or later
(sb-box-base-<sha>-2026-10-13 onwards) carries a code interpreter (a
Jupyter Python kernel), started on the first call (about 5 s once per
sandbox; later cells about 2 s). Variables, imports and DataFrames persist
between calls like a notebook and survive freeze → resume. (-s "$ID" below
can be --session <name> — §14.1.)
gsk sb-box run-code -s "$ID" --code 'import pandas as pd
df = pd.DataFrame({"region": ["east", "west"], "margin": [0.31, 0.12]})
df'
# → data = {"stdout": "", "stderr": "", "executionCount": 1, "complete": true, "error": null,
# "results": [{"text": " region margin\n0 east 0.31\n1 west 0.12",
# "html": "<div>…</div>", "data": {"region": ["east","west"], "margin": [0.31, 0.12]},
# "isMainResult": true}]}
gsk sb-box run-code -s "$ID" --code 'import matplotlib.pyplot as plt
plt.bar(df.region, df.margin); plt.show()'
# → results: [{"omitted": {"png": 16044}, "chart": {"type": "bar", "elements": [...]}}] # --images base64 to inline the png
gsk sb-box run-code -s "$ID" --code '1/0'
# → cellError = {"name": "ZeroDivisionError", "value": "division by zero", "traceback": "…"}; the kernel survives
# (REST calls this field `error`)
curl -s -X POST "$API/sandboxes/$SBX/run-code" -H "$H" -H 'Content-Type: application/json' \
-d '{"code": "df.margin.sum()", "timeout": 60}'
| Field | Meaning |
|---|---|
code (required) |
the cell |
language |
kernel language; default python (other kernels only if the template ships them) |
contextId |
run in a specific interpreter context; default = the sandbox's default Python context |
envVars |
extra environment for this cell |
timeout |
the cell's budget in seconds (default 60, max 540); on expiry the reply carries error.name = "InterpreterTimeout" |
images |
REST default base64 (png/jpeg/pdf inline); the CLI defaults to omit, which drops them and reports their sizes under results[].omitted |
results[] is a list of MIME bundles exactly as the kernel displayed them:
text, html, markdown, svg, png, jpeg, pdf, latex, json,
javascript, plus two structured forms — data (a DataFrame as columns) and
chart (a matplotlib figure decoded into type, axes and series). The
interpreter ships in the public base template only: a template built
from your own image, or a public-base sandbox predating 2026-10-13, answers
409 sb_box_interpreter_unavailable (installing ipykernel yourself does
not help — use exec with a script). 502 sb_box_interpreter_start_failed
carries the start log.
The interpreter also answers directly on port 49999
(https://49999-<id>.sandbox.…/execute), under the sandbox's normal
public-port rules: on a default sandbox (allowPublicTraffic: true) anyone
who knows the sandbox id can post code to it. If that is not acceptable,
create the sandbox with allowPublicTraffic: false and send the traffic
token (§10.1).
9. Filesystem
Files move by path; the body is the content.
# write (parents are created)
curl -s -X POST "$API/sandboxes/$SBX/files/stream?path=/home/user/data/in.csv" -H "$H" --data-binary @in.csv
# → {"written":20}
# read
curl -s "$API/sandboxes/$SBX/files/stream?path=/home/user/out.json" -H "$H" -o out.json
# list
curl -s "$API/sandboxes/$SBX/files/list?path=/home/user" -H "$H"
# → [{"name":"data","path":"/home/user/data","isDir":true,"size":4096},
# {"name":"in.txt","path":"/home/user/in.txt","isDir":false,"size":20}, …]
CLI: write --path … --content …, read --path …, ls --path … (text
content; the CLI decodes as UTF-8 and replaces undecodable bytes). Binary
round-trips are byte-exact over REST.
| Query | Meaning |
|---|---|
path |
Absolute, or relative to the user's home (path=rel.txt → /home/user/rel.txt). Required for write and read; files/list defaults to /. |
user |
Omit for the image's default user (user), or root. |
- A root-only file answers
403withoutuser=root; a missing path answers404. - The guest needs GNU coreutils (any Debian/Ubuntu image); Alpine-only images are not supported for file operations.
Every sandbox gets a 20 GB root disk, billed running or frozen with no free allowance (billing §1, §2).
10. Networking
10.1 Public URL per port
Anything listening on a port inside the sandbox is reachable at
https://<port>-<sandbox-id>.sandbox.<region>-prod.sspark.ai
Read the host from the sandbox record rather than building it: portUrl is
this URL with a literal {port} placeholder, domain the suffix after
<port>-<sandbox-id>. (the suffix differs per region).
curl -s -X POST "$API/sandboxes/$SBX/commands/start" -H "$H" -H 'Content-Type: application/json' \
-d '{"command": ["python3", "-m", "http.server", "8000", "--directory", "/home/user"]}'
PORT_URL=$(curl -s "$API/sandboxes/$SBX" -H "$H" | jq -r .portUrl) # https://{port}-sbx-….sandbox.westus2-prod.sspark.ai
curl -s "${PORT_URL/\{port\}/8000}/data/in.txt"
# → hello from the guide
A request to a frozen sandbox resumes it first (a second or two), so a preview server or webhook receiver costs nothing while idle.
Two network options on create shape these URLs:
-
allowPublicTraffic: falsegates every port URL of the sandbox behind a token. The create response then carriestrafficAccessToken; send it as theX-Sb-Traffic-Tokenheader. Without it a request answers401.curl -s -X POST "$API/sandboxes" -H "$H" -H 'Content-Type: application/json' \ -d '{"templateId": "tpl-7b7c…", "timeout": 600, "network": {"allowPublicTraffic": false}}' # → {"id": "sbx-0b3e…", …, "trafficAccessToken": "6c5bfcbd-…"} curl -s -o /dev/null -w '%{http_code}\n' https://8000-sbx-0b3e….sandbox.westus2-prod.sspark.ai/ # 401 curl -s -o /dev/null -w '%{http_code}\n' -H 'X-Sb-Traffic-Token: 6c5bfcbd-…' https://8000-sbx-0b3e….sandbox.westus2-prod.sspark.ai/ # 200 -
maskRequestHostrewrites theHostheader your service sees, for apps that validate it;${PORT}is substituted ("app-${PORT}.internal"→ a server on 8000 seesHost: app-8000.internal).
10.2 Internet access and egress rules
Outbound internet is on by default. Restrict it per sandbox with the
network block on create:
curl -s -X POST "$API/sandboxes" -H "$H" -H 'Content-Type: application/json' -d '{
"templateId": "tpl-7b7c…", "timeout": 0,
"network": {
"allowInternetAccess": false, # default true
"allowOut": ["10.20.0.0/16", "1.1.1.1/32"], # IPs / CIDRs that stay reachable
"denyOut": [] # IPs / CIDRs to block while internet is on
}
}'
network. field |
Type · default | Meaning |
|---|---|---|
allowInternetAccess |
bool · true |
false blocks all outbound traffic except allowOut. |
allowOut |
array of IPs / CIDRs · [] |
Destinations that stay reachable when internet access is off. |
denyOut |
array of IPs / CIDRs · [] |
Destinations blocked while internet access is on. |
allowPublicTraffic |
bool · true |
false puts every port URL behind a token (10.1). |
maskRequestHost |
string · none | Host header your service sees on port-URL requests; ${PORT} is substituted (10.1). |
Rules are IPs and CIDRs only; hostnames are rejected at create:
400 {"detail": "invalid network policy: allowOut: not an IP or CIDR (domains not yet supported): \"api.example.com\""}
The policy is fixed at create time. The CLI does not expose network; use REST.
11. Sandbox metrics
curl -s "$API/sandboxes/$SBX/metrics" -H "$H"
# → [{"timestamp":"2026-09-21T11:32:33Z","timestampUnix":1789990353,"cpuCount":2,"cpuUsedPct":0,
# "memUsed":98181120,"memTotal":4196507648,"memCache":75390976,
# "diskUsed":2837368832,"diskTotal":20957446144}, …]
One sample every 5 s while the guest runs; bytes for memory and disk, percent
of all cores for CPU. A frozen sandbox answers with the samples recorded
while it ran; a killed one answers 409.
| Query | Meaning |
|---|---|
start, end |
Unix seconds (integers). Default window: the last hour up to now. At most 1,200 samples are returned. |
12. Sharing
Grant another Genspark user access to a sandbox or a template. read lets them
see it (and, for a template, create from it); write lets them operate a
sandbox (exec, files, freeze/resume, clone). Kill and reset stay owner-only.
curl -s -X PUT "$API/sandboxes/$SBX/grants/$USER_ID" -H "$H" -H 'Content-Type: application/json' \
-d '{"perm": "read", "expires_at": "2026-12-31T00:00:00Z"}'
# → {"sandbox_id":"sbx-…","grantee_id":"…","permission":"read","granted_at":"…","expires_at":"2026-12-31T00:00:00.000000+00:00"}
curl -s "$API/sandboxes/$SBX/grants" -H "$H" # list
curl -s -X DELETE "$API/sandboxes/$SBX/grants/$USER_ID" -H "$H" # → {"status":"revoked"}
Templates: the same three calls under /templates/{id}/grants/{userId}.
| Body | Meaning |
|---|---|
perm |
read or write (default read); anything else is 400 invalid permission |
expires_at |
Optional RFC 3339 timestamp such as 2026-12-31T00:00:00Z, or a date YYYY-MM-DD (= midnight UTC); the grant lapses then. |
reason |
Optional free text for your own records; not returned |
{userId} is the other user's Genspark id. Grantees list what is shared with
them via GET /sandboxes/shared and GET /templates/shared (no console tab).
Console for granting: sandbox or template page → Sharing / Grants.
13. Templates
A template is a booted, snapshotted environment. Put everything slow —
apt, pip, model downloads, warm caches — into the template, not into
each run.
13.1 Build
gsk sb-box build-template --template py312 --image python:3.12-slim \
--cpu_count 2 --memory_mb 2048 --disk_size_gb 20 \
--start_cmd 'pip install -q numpy pandas && touch /tmp/ready && sleep infinity' \
--ready_cmd 'test -f /tmp/ready'
# → {"templateId": "tpl-…", "status": "building", "region": "westus2", "owner": "sbbox/<your user id>"}
curl -s -X POST "$API/templates/build" -H "$H" -H 'Content-Type: application/json' -d '{
"name": "py312",
"image": "python:3.12-slim",
"cpuCount": 2, "memoryMB": 2048, "diskSizeGB": 20,
"startCmd": "pip install -q numpy pandas && touch /tmp/ready && sleep infinity",
"readyCmd": "test -f /tmp/ready",
"description": "Python 3.12 with numpy and pandas"
}'
| Field | REST | gsk | Notes |
|---|---|---|---|
| name | name |
--template |
shown in lists; what templateName looks up |
| source | image or dockerfile |
--image |
a public container image, or a Dockerfile as a string (REST only) |
| CPU | cpuCount (default 1) |
--cpu_count (1) |
up to your plan's cap |
| memory | memoryMB (default 2048) |
--memory_mb (2048) |
up to your plan's cap; the public base itself is 2 vCPU / 4 GB |
| disk | diskSizeGB (default = your plan's cap) |
--disk_size_gb (20) |
20 GB is the cap on every plan today |
| start command | startCmd |
--start_cmd |
must end in a long-running process (default sleep infinity) |
| readiness | readyCmd |
--ready_cmd |
polled up to 60 s; exit 0 = ready to snapshot |
| region | region |
--region |
which region builds and hosts it (see 13.4); omit for the default |
| description | description |
— | free text shown in the console |
| visibility | namespace |
--namespace |
omit = private (share it with a grant). The public catalog is published by Genspark. |
Poll until status is ready:
gsk sb-box templates | jq '.data.templates[] | select(.name=="py312") | .status'
curl -s "$API/templates/$TPL" -H "$H"
# → {"id":"tpl-…","name":"py312","namespace":"…","base_image":"python:3.12-slim","region":"westus2",
# "status":"ready","is_owner":true,"can_create":true,"created_at":"…","updated_at":"…"}
The template record:
| Field | Meaning |
|---|---|
id |
tpl-…; use it as templateId |
name |
what templateName resolves; several templates may share a name (the newest ready one wins) |
description |
your free text |
namespace |
public for the shared catalog; otherwise the owner's account id (a private template) |
base_image |
the image it was built from, or dockerfile |
status |
building → ready, or error when the build failed |
region |
where it was built and where its sandboxes run |
is_owner, can_create |
whether you own it, and whether you may create sandboxes from it right now (false while building) |
cogen_id |
the owner's account id (shown on your own and on shared templates) |
created_at, updated_at |
timestamps |
A build takes about 5–7 minutes for a small image, longer when startCmd
installs packages. Up to 5 builds run at once per account (billing §2).
A failed build reports status: error with the reason in buildError
(§15.2); the usual causes are a wrong image name, a
startCmd that exits, or a readyCmd that cannot pass within 60 s.
13.2 How the build works: start and ready commands
The build boots the image, runs startCmd, polls readyCmd until it exits 0
(up to 60 s), then snapshots memory and disk. Whatever startCmd left
running is already running when a sandbox comes up.
Two rules:
- End
startCmdwith a marker and probe the marker. A probe likepython3 -c "import pandas"can pass whilepipis still running, and the snapshot then captures a half-finished install.… && touch /tmp/ready && sleep infinitywithreadyCmdtest -f /tmp/readycannot. - Probe what sandboxes will actually use — a port listening, a virtualenv importing.
startCmd runs once, at build time. It does not re-run when a sandbox is
created; use exec or a background command for per-sandbox setup.
13.3 Names, rebuilds and the public catalog
templateNameresolves to the newest ready template with that name you can see, so a rebuild under the same name rolls forward without changing callers.baseis an alias for the newest ready public base.- Your templates are private by default; share one with a grant
(§12). The public catalog (
namespace: public, names likesb-box-base-<sha>-<date>) is published by Genspark and usable by everyone. DELETE /templates/{id}(gsk sb-box delete-template --template tpl-…) removes a template you own; sandboxes already created from it keep running. AfterwardsGET /templates/{id}answers404 template not found.
13.4 Regions
Templates are region-scoped: create always lands on the template's
region. GET /regions lists the regions and the default:
{"regions":["default","westus2"],"default":"westus2"}
14. gsk sb-box CLI reference
gsk sb-box <action> [--flag value …]. Every action is a thin call to the
REST API, made as you.
| Action | Required | Optional | REST · what you get back |
|---|---|---|---|
templates |
— | — | GET /templates · {"templates": […]} |
build-template |
--image |
--template (name; default = the image tag), --cpu_count (1), --memory_mb (2048), --disk_size_gb (20), --start_cmd, --ready_cmd, --region, --log_paths |
POST /templates/build · {"templateId","status":"building","region","owner"} |
delete-template |
--template tpl-… |
— | DELETE /templates/{id} |
create |
— | --template (name, base, or tpl-… id; required), --timeout (seconds; default none; frozen at the deadline), --session |
POST /sandboxes · the sandbox record |
list |
— | — | GET /sandboxes · {"sandboxes": […], "hasMore": false} |
get |
-s |
— | GET /sandboxes/{id} · the sandbox record |
exec |
-s, -c |
--timeout (seconds; default 30, max 600) |
POST /sandboxes/{id}/exec (runs /bin/sh -c <command>) · {"stdout","stderr","exitCode"} |
run-code |
-s, --code |
--language (python), --context_id, --timeout (seconds; default 60, max 540), --images (omit · base64) |
POST /sandboxes/{id}/run-code · {"stdout","stderr","results":[…],"cellError","executionCount"} (REST: error) — a cell on the sandbox's stateful interpreter (§8.5); the first call starts it |
write |
-s, -p, -d |
— | POST /sandboxes/{id}/files/stream · {"written","path"} |
read |
-s, -p |
— | GET /sandboxes/{id}/files/stream · {"content","path"} |
ls |
-s |
-p (default /) |
GET /sandboxes/{id}/files/list · the listing |
freeze |
-s |
— | POST /sandboxes/{id}/freeze · the frozen record ("state":"frozen"); only a freeze that outruns ~50 s answers freezing — then poll get |
resume |
-s |
— | POST /sandboxes/{id}/resume · the sandbox record |
versions |
-s |
— | GET /sandboxes/{id} → {"sandbox_id","state","versions":[{version,frozenAt,label}]} |
clone |
-s |
--version (omit = the live state) |
POST /sandboxes/{id}/clone · the new sandbox's record |
rollback |
-s, --version (0 = last committed freeze) |
— | POST /sandboxes/{id}/rollback · the sandbox record |
timeout |
-s, --timeout (seconds from now) |
— | POST /sandboxes/{id}/timeout · {"status":"ok"} |
kill |
-s |
— | DELETE /sandboxes/{id} · {"status":"killed","sandbox_id"} |
describe |
— | — | prints the built-in usage guide |
Flags:
| Flag | Actions | Meaning |
|---|---|---|
-s, --sandbox_id |
get, kill, exec, run-code, write, read, ls, freeze, resume, rollback, clone, versions, timeout | the sandbox id (sbx-…) |
--session |
the same actions, plus create | a session name in place of --sandbox_id: the CLI remembers which sandbox the name owns and reuses it; run-code / exec / write create one when there is none (§14.1) |
-t, --template |
create, build-template, delete-template | create: template name or tpl-… id (default: the newest ready public base). build-template: the new template's name (default: the image tag). delete-template: the tpl-… id |
-c, --command |
exec | shell command line, run as /bin/sh -c <command> |
--code |
run-code | the cell's source |
--language, --context_id, --images |
run-code | kernel language (default python); interpreter context id; omit (default) or base64 for png/jpeg/pdf results |
-p, --path |
write, read, ls | file path, absolute or relative to /home/user; ls defaults to / |
-d, --content |
write | file content (text) |
--timeout |
create, timeout, exec, run-code | create: lifetime in seconds (default 0 = no deadline; a deadline freezes the sandbox). timeout: new lifetime in seconds from now. exec: command timeout in seconds (default 30, max 600). run-code: the cell's budget in seconds (default 60, max 540) |
--version |
rollback, clone | rollback: 0 = the last committed freeze (default), N = archived version N. clone: fork archived version N; omit to fork the live state |
--image |
build-template | container image to build from (required) |
--cpu_count, --memory_mb, --disk_size_gb |
build-template | sandbox size: vCPUs (default 1), memory in MB (2048), disk in GB (20); capped by your plan |
--start_cmd, --ready_cmd |
build-template | the start and readiness commands (§13.2); --start_cmd defaults to sleep infinity |
--region |
build-template | region to build in; omit for the default. create always lands on the template's region |
--log_paths |
build-template | comma-separated absolute file globs inside the guest to collect (max 8) |
--args-file |
any | path to a JSON object (- = stdin) with server parameters; file keys override flags |
--args-file carries parameters the shell would mangle ($, quotes,
newlines):
cat > req.json <<'JSON'
{"sandbox_id": "sbx-…", "path": "/home/user/run.sh", "content": "echo \"$HOME\"\ncd /tmp\n"}
JSON
gsk sb-box write --args-file req.json
Not reachable from the CLI (use REST): metadata, network and
lifecycle.onTimeout on create; timeout on resume / rollback; reset,
metrics, streaming exec, background commands, PTY, sharing grants, GET /usage, GET /limits.
The envelope. Every CLI call prints exactly one JSON object:
| Field | Meaning |
|---|---|
version |
envelope format version (1) |
status |
ok or error — check this, not the process exit code |
message |
success, or the reason: the API's HTTP status and URL, or a local validation message |
data |
the API response as documented in this guide (null on error) |
session_state |
unused for sb-box; always null |
An API refusal:
{"version": 1, "status": "error",
"message": "Client error '404 Not Found' for url '…/api/sb-box/v1/sandboxes/sbx-doesnotexist'",
"data": null}
A local error (e.g. no ready public base template to create from; pass --template <name|tpl-id>) uses the same envelope.
14.1 Sessions: --session instead of a sandbox id
--session <name> keeps every command of one task on the same sandbox
without carrying an id:
gsk sb-box run-code --session q3-report --code 'import pandas as pd; print(pd.__version__)'
# → data = {…, "session": {"key": "q3-report", "sandbox_id": "sbx-4321f1…", "created": true}}
gsk sb-box write --session q3-report --path /home/user/data.csv --content "$(cat data.csv)"
gsk sb-box run-code --session q3-report --code 'df = pd.read_csv("/home/user/data.csv"); df.describe()'
gsk sb-box kill --session q3-report # also drops the binding
- The first
run-code,execorwriteon a name creates a sandbox (from the newest public base template, or--template <name|tpl-id>if given; no hard deadline) and binds the name to it. Later commands reuse it (a frozen one is resumed). A sandbox that was killed or hit a deadline is replaced on the next creating command. get,read,ls,kill,freeze,resume, … only use an existing binding and fail with a clear message when there is none — they never create.create --sessionbinds the new sandbox explicitly.- An explicit
--sandbox_idalways wins and leaves the binding alone. - Without the flag the name comes from the environment:
GSK_SB_BOX_SESSIONif set, else the session id a host agent exports (CLAUDE_CODE_SESSION_IDinside Claude Code), so a baregsk sb-box run-code --code …in Claude Code runs on that conversation's sandbox. With no name anywhere:sandbox_id required. - Every successful call echoes the binding as
data.session(key,sandbox_id,created). Bindings live in~/.genspark-tool-cli/sb-box-sessions.json, scoped by API host, on the machine that runsgsk— a different machine is a different session.
14.2 Use SB-Box from a coding agent
A coding agent with gsk on its PATH can use SB-Box through one skill file.
Install it into a project:
curl -fsSL https://www.genspark.ai/sb-box/opencode/install.sh | sh
# wrote .opencode/skills/sb-box/SKILL.md
SKILL.md is an
agent skill the model loads on demand.
It tells the model to name each task's sandbox with --session
(§14.1), copy files in
with write, use run-code (§8.5)
for analysis and exec for scripts, read results back and kill when
done. Works with any agent that reads SKILL.md skills; opencode looks in
.opencode/skills/. Inside Claude Code --session is optional
(§14.1).
Requirements: npm i -g @genspark/cli && gsk login. The
install script never overwrites existing
files. The intro page shows a recorded session.
15. REST API reference
Beta. The REST API is in beta: the routes below are supported, but response fields may still be added and the rough edges noted in §19 are being worked on. The CLI and console are the stable surfaces.
Base URL https://www.genspark.ai/api/sb-box/v1.
The former /api/sandbox/v1 prefix still answers, with Deprecation and
Link: <…>; rel="successor-version" headers. Every
request needs the key (§2): X-Api-Key: gsk-…
or Authorization: Bearer gsk-…. Without a key HTTP calls get a 401 JSON
body and …/ws/pty refuses the handshake. Request bodies are JSON
(Content-Type: application/json) unless a section says otherwise; every
lifetime is in seconds.
Conventions used below:
- Auth — who may call it: any (your key), write (the owner or a user
holding a
writegrant on the sandbox), owner (only the creator). - Required — a parameter without a default; the call answers
400without it. - Errors are JSON, in one of two shapes —
{"detail": …}(a string for ordinary errors, an object for the plan and billing refusals) or{"error": …}on the command routes (§16 maps routes to shapes). Each endpoint lists its codes beyond the common set. - Common to every endpoint:
401 authentication required(no key),403 SB-Box is not enabled for this account(access not yet granted),404 sandbox not found/404 template not found(unknown id, or not yours and not shared — private resources never answer403),422(a query parameter missing or of the wrong type — the body lists each offending parameter undererrors[]),500/503(platform error, region unavailable — retry). Platform refusals pass through unchanged asdetail.
Examples use $SBX, $TPL, $USER for ids and shorten timestamps.
15.1 Regions, limits, usage
GET /regions — list regions
The regions and the default for new templates. No parameters.
{"regions": ["default", "westus2"], "default": "westus2"}
| Code | Meaning |
|---|---|
200 |
the list |
GET /limits — your plan and standing
Your plan, its caps and current usage (billing §2). No parameters.
{"plan": "plus",
"limits": {"maxConcurrentSandboxes": 50, "maxCpu": 8, "maxMemoryMB": 8192, "maxDiskGB": 20, "maxConcurrentBuilds": 5},
"usage": {"runningSandboxes": 3, "buildingTemplates": 0}}
| Field | Meaning |
|---|---|
plan |
free · plus · pro · team_plus · enterprise |
limits |
the caps of that plan |
usage |
live counts against the concurrency caps |
| Code | Meaning |
|---|---|
200 |
the record |
GET /usage — metered usage and credits
Per-UTC-day run time, vCPU-seconds, GB-seconds and the credits debited (billing §3).
| Query | Type · default | Meaning |
|---|---|---|
days |
integer 1–366 · 30 |
how many days back from today |
{"days": 7, "from": "2026-09-15", "to": "2026-09-21",
"rateCard": {"creditsPerCpuCoreHour": 25.0, "creditsPerGbMemHour": 8.0, "creditsPerGbDiskHour": 0.05, "diskBilled": true},
"direct": {"runSec": 6950, "cpuCoreSec": 13900, "memGBSec": 16045, "diskGBSec": 5184000, "credits": 204.1833},
"daily": [{"day": "20260921",
"direct": {"run_sec": 6950.0, "cpu_core_sec": 13900.0, "mem_gb_sec": 16045.0, "disk_gb_sec": 5184000.0, "credits": 204.1833}}]}
direct is your own usage (the only bucket debited). Days with no usage are
omitted; daily[] uses YYYYMMDD and snake_case.
| Code | Meaning |
|---|---|
200 |
the report |
422 |
days outside 1–366 or not an integer |
15.2 Templates
The template record returned by the template endpoints is described in
§13.1: id, name, description, namespace (public or the
owner's account id), base_image, status (building · ready · error),
buildError (why, when error), region, source, is_owner,
can_create, cogen_id, created_at, updated_at.
POST /templates/build — build a template
Boots the image, runs startCmd, waits for readyCmd, snapshots the
result. Asynchronous: poll GET /templates/{id} until status is ready.
Auth: any.
| Body | Type · default | Meaning |
|---|---|---|
name |
string · the image tag | shown in lists; what templateName looks up |
image |
string | a public container image (python:3.12-slim). One of image / dockerfile is required |
dockerfile |
string | a Dockerfile as text, built for you (alternative to image) |
cpuCount |
integer · 1 |
vCPUs; ≤ your plan's cap (409 sb_box_spec_limit over it) |
memoryMB |
integer · 2048 |
memory; ≤ your plan's cap |
diskSizeGB |
integer · 20 |
disk; 20 is the only accepted value today (over it → 400) |
startCmd |
string · sleep infinity |
runs at build time; must end in a long-running process |
readyCmd |
string · none | polled up to 60 s; exit 0 = ready to snapshot |
region |
string · the default region | which region builds and hosts it (§13.4) |
logPaths |
array of strings | up to 8 absolute file globs inside the guest to collect |
description |
string | free text |
curl -s -X POST "$API/templates/build" -H "$H" -H 'Content-Type: application/json' -d '{
"name": "py312", "image": "python:3.12-slim", "cpuCount": 2, "memoryMB": 2048, "diskSizeGB": 20,
"startCmd": "pip install -q numpy pandas && touch /tmp/ready && sleep infinity",
"readyCmd": "test -f /tmp/ready"}'
{"templateId": "tpl-875c…", "status": "building", "region": "westus2", "owner": "sbbox/<your user id>"}
| Code | Meaning |
|---|---|
200 |
build started; poll the template |
400 |
dockerfile or image is required; diskSizeGB N exceeds the M GB limit for user templates on this plan; cpuCount / memoryMB must be integers |
409 |
{"code": "sb_box_spec_limit", …} cpu or memory over your cap; {"code": "sb_box_build_concurrency_limit", …} five builds already running |
GET /templates — list templates you can create from
Your own templates plus the public catalog (shared-with-you ones are under
/templates/shared). Auth: any.
{"templates": [
{"id": "tpl-7b7c…", "name": "sb-box-base-<sha>-<date>", "namespace": "public", "base_image": "…/sb-box-template-base:…",
"status": "ready", "region": "westus2", "source": "user", "is_owner": false, "can_create": true,
"created_at": "2026-09-20T11:25:45+00:00", "updated_at": "2026-09-20T11:25:45+00:00"},
{"id": "tpl-b84b…", "name": "py312", "namespace": "5f63…", "base_image": "python:3.12-slim", "status": "ready", …, "is_owner": true}]}
| Code | Meaning |
|---|---|
200 |
the list (possibly empty) |
GET /templates/shared — templates shared with you
Templates another user granted you. Auth: any. No parameters. Answers a
JSON array of template records ([] when none).
| Code | Meaning |
|---|---|
200 |
the array |
GET /templates/{id} — one template
The record with live build status. Auth: any user who can see it.
{"id": "tpl-875c…", "name": "verify-df", "namespace": "5f63…", "base_image": "dockerfile", "status": "error",
"buildError": "build dispatch failed: …", "region": "westus2", "source": "user", "is_owner": true, "can_create": false,
"created_at": "…", "updated_at": "…", "cogen_id": "5f63…"}
| Code | Meaning |
|---|---|
200 |
the record |
404 |
template not found — unknown, or private and not shared with you |
DELETE /templates/{id} — delete a template
Sandboxes already created from it keep running. Auth: owner.
{"status": "deleted"}
| Code | Meaning |
|---|---|
200 |
deleted |
404 |
not yours, or unknown |
PUT /templates/{id}/grants/{userId} — share a template
Lets another user ({userId} = account id) see and create from it. Auth: owner.
| Body | Type · default | Meaning |
|---|---|---|
perm |
read | write · read |
read = see it and create from it; write additionally lets them operate it |
expires_at |
RFC 3339 timestamp or YYYY-MM-DD · none |
when the grant lapses (a date = midnight UTC) |
reason |
string | for your records; not returned |
{"template_id": "tpl-b84b…", "grantee_id": "0000…abcd", "permission": "read",
"granted_at": "2026-09-21T11:36:26.946265+00:00", "expires_at": "2026-12-31T00:00:00.000000+00:00"}
| Code | Meaning |
|---|---|
200 |
grant created or updated |
400 |
invalid permission: <value>; invalid expires_at: … |
404 |
not your template |
GET /templates/{id}/grants — list a template's grants
Auth: owner. No parameters.
[{"grantee_id": "0000…abcd", "permission": "read", "granted_at": "…", "expires_at": null}]
| Code | Meaning |
|---|---|
200 |
the array ([] when none) |
DELETE /templates/{id}/grants/{userId} — revoke a grant
Auth: owner. No body.
{"status": "revoked"}
| Code | Meaning |
|---|---|
200 |
revoked (also when there was nothing to revoke) |
15.3 Sandboxes: lifecycle
The sandbox record and its state values are described in §3.3.
POST /sandboxes — create a sandbox
Restores a template snapshot and returns when it is running (about half a second). Auth: any; the creator is the owner.
| Body | Type · default | Meaning |
|---|---|---|
templateName |
string | name of a template you can see; the newest ready one wins. One of templateName / templateId is required; templateName takes precedence |
templateId |
string | exact tpl-… id |
templateNamespace |
string · omit | restrict the name lookup: public = the shared catalog only; anything else → 400 |
timeout |
number (seconds) · none | lifetime; omitted or 0 = no deadline. timeoutMs (milliseconds) is deprecated (Deprecation: true + Warning: 299 … headers) |
lifecycle.onTimeout |
freeze | kill · freeze |
what happens at the deadline |
metadata |
object of strings · {} |
your key/values, echoed on every read |
envVars |
object of strings · {} |
sandbox-wide environment for every command (§7) |
network.allowInternetAccess |
bool · true |
false blocks outbound traffic except allowOut |
network.allowOut |
array of IPs/CIDRs · [] |
reachable destinations while internet is off |
network.denyOut |
array of IPs/CIDRs · [] |
blocked destinations while internet is on |
network.allowPublicTraffic |
bool · true |
false gates the port URLs behind a token (§10.1) |
network.maskRequestHost |
string · none | Host header your service sees on port-URL requests; ${PORT} is substituted |
namespace |
string · omit | a grouping label, stored as <your user id>/<label>; filter with GET /sandboxes?namespace=<label> |
curl -s -X POST "$API/sandboxes" -H "$H" -H 'Content-Type: application/json' -d '{
"templateName": "py312", "timeout": 900,
"metadata": {"purpose": "docs", "userId": "u-123"},
"network": {"allowPublicTraffic": false}}'
{"id": "sbx-44fb…", "templateId": "tpl-7b7c…", "state": "running", "region": "westus2",
"portUrl": "https://{port}-sbx-44fb….sandbox.westus2-prod.sspark.ai", "domain": "sandbox.westus2-prod.sspark.ai",
"createdAt": "2026-09-21T11:32:30Z", "startedAt": "2026-09-21T11:32:31.760511059Z", "timeoutAt": "2026-09-21T11:47:30Z",
"cpuCount": 2, "memoryMB": 4096, "diskSizeGB": 20, "idleTimeout": 600,
"metadata": {"purpose": "docs", "userId": "u-123", "cogenId": "5f63…"},
"trafficAccessToken": "6c5bfcbd-…"}
trafficAccessToken appears only when allowPublicTraffic is false, and only here.
| Code | Meaning |
|---|---|
200 |
running |
400 |
templateId or templateName is required — pick one from GET /templates; template is not ready (status: building); invalid timeout: 'abc' (must be a number); invalid network policy: allowOut: not an IP or CIDR (domains not yet supported): "…" |
402 |
{"code": "sb_box_insufficient_credits", "balance": "0.00", "message": "…"} (billing §1) |
404 |
no ready template with name '…'; template not found |
409 |
{"code": "sb_box_concurrency_limit" | "sb_box_spec_limit", …} (billing §2) |
499 |
client disconnected — you hung up before provisioning finished; nothing was created |
504 |
<phase> timed out; please retry — the template lookup exceeded its 20 s budget |
Gates run in this order: template access → not ready → credits → plan caps.
GET /sandboxes — list your sandboxes
Compact rows for what you created, newest first. Auth: any.
| Query | Type · default | Meaning |
|---|---|---|
state |
comma-separated states · all | keep only running, starting, freezing, frozen, exited, killed |
namespace |
string | only rows created with that namespace label (stored form <your user id>/<label>) |
{"sandboxes": [{"id": "sbx-44fb…", "template_id": "tpl-7b7c…", "state": "running", "region": "westus2", "source": "user",
"portUrl": "https://{port}-sbx-44fb….sandbox.westus2-prod.sspark.ai", "domain": "sandbox.westus2-prod.sspark.ai",
"namespace": "ns_2eeb…", "cogen_id": "5f63…", "created_at": "2026-09-21T11:32:31.845822+00:00", "updated_at": "…"}],
"hasMore": false}
state is unknown when the region could not be read. updated_at moves
with each lifecycle transition, usually within seconds; for exact moments
read startedAt / frozenAt on the full record. No pagination yet
(hasMore is always false).
| Code | Meaning |
|---|---|
200 |
the list (possibly empty) |
GET /sandboxes/shared — sandboxes shared with you
Sandboxes another user granted you access to. Auth: any. No parameters;
answers a JSON array of records ([] when none).
| Code | Meaning |
|---|---|
200 |
the array |
GET /sandboxes/{id} — one sandbox
The full record (§3.3), with versions[] and
frozenAt once frozen, plus source, namespace, is_owner,
has_write. Auth: any user who can see it.
{"id": "sbx-44fb…", "templateId": "tpl-7b7c…", "state": "frozen", "region": "westus2",
"createdAt": "…", "startedAt": "…", "frozenAt": "2026-09-21T11:34:51Z", "timeoutAt": "…",
"cpuCount": 2, "memoryMB": 4096, "diskSizeGB": 20, "idleTimeout": 600,
"versions": [{"version": 1, "frozenAt": "2026-09-21T11:33:10.922641058Z", "label": "auto"},
{"version": 2, "frozenAt": "2026-09-21T11:34:44.162637328Z", "label": "auto"}],
"metadata": {"purpose": "docs", "cogenId": "5f63…"},
"source": "user", "namespace": "ns_2eeb…", "is_owner": true, "has_write": true,
"cogen_id": "5f63…", "template_id": "tpl-7b7c…", "created_at": "…", "updated_at": "…"}
| Code | Meaning |
|---|---|
200 |
the record |
404 |
sandbox not found — unknown, killed, or not yours and not shared |
DELETE /sandboxes/{id} — kill a sandbox
Irreversible: the machine and its disk are gone. Auth: owner.
{"status": "killed"}
| Code | Meaning |
|---|---|
200 |
killed |
404 |
unknown, already killed, or not yours |
POST /sandboxes/{id}/freeze — freeze (save point)
Snapshots memory and disk to storage, releases the machine, archives the replaced snapshot as a version (§4, §5). Compute stops billing while frozen; the disk keeps billing (billing §1). Any later call or port request resumes it. Auth: write. No body. Waits for the snapshot (a few seconds):
{"id": "sbx-3f2c…", "templateId": "tpl-3218…", "state": "frozen", "createdAt": "…", "startedAt": "…",
"frozenAt": "2026-09-22T04:07:54Z", "timeoutAt": "…", "cpuCount": 2, "memoryMB": 4096, "diskSizeGB": 20,
"versions": [{"version": 1, "frozenAt": "2026-09-22T04:07:54.657154572Z", "label": "auto"}], "idleTimeout": 600, "metadata": {…}}
| Code | Meaning |
|---|---|
200 |
frozen — the record above; on an already-frozen sandbox {"state": "frozen", "status": "already frozen"} |
202 |
{"state": "freezing", "status": "freeze in progress"} — only when the snapshot outran the platform's wait (about 50 s); poll GET until frozen |
409 |
the sandbox is not running (is not running (state=…)), or the freeze was cancelled by a concurrent resume |
500 |
freeze failed — the snapshot export failed |
Each freeze appends to versions[] the save point it replaced: after
the first freeze, version 1 is the starting state (its frozenAt is the
creation time); after the second, version 2 is the first freeze's state.
The newest save point is always version: 0 on /rollback and /clone.
POST /sandboxes/{id}/resume — resume
Brings a frozen sandbox back, processes and memory intact, in about a second. Idempotent on a running sandbox. Auth: write.
| Body | Type · default | Meaning |
|---|---|---|
timeout |
number (seconds) · none | re-grant this lifetime from now (also applied when already running) |
version |
integer | deprecated on this route: a non-zero value turns the call into POST /rollback with that version |
{"id": "sbx-44fb…", "templateId": "tpl-7b7c…", "state": "running", "startedAt": "2026-09-21T11:34:45Z",
"frozenAt": "2026-09-21T11:34:44Z", "timeoutAt": "2026-09-21T11:44:47Z", "cpuCount": 2, "memoryMB": 4096, "diskSizeGB": 20,
"idleTimeout": 600, "versions": […], "metadata": {…}}
| Code | Meaning |
|---|---|
200 |
running (already running = unchanged record) |
402 |
credits used up (billing §1) |
409 |
{"code": "sb_box_concurrency_limit", …} — resuming would exceed your running cap |
428 |
{"reason": "memory_expired", "recovery": ["reset"]} (top level, not under detail) — the memory snapshot was reclaimed after 14 days frozen (§4.1); reset restarts it from disk |
410 |
disk_lost — the disk is gone too; recreate |
POST /sandboxes/{id}/rollback — roll back to a save point
Puts the sandbox back at a save point and resumes it. The state being left is archived first, so a rollback is reversible. Needs a settled sandbox. Auth: write.
| Body | Type · default | Meaning |
|---|---|---|
version |
integer · 0 |
0 = the newest save point (the last freeze); N = archived version N from versions[] — the state that freeze N replaced (see /freeze) |
timeout |
number (seconds) · none | re-grant this lifetime from now |
{"id": "sbx-44fb…", "state": "running", "startedAt": "2026-09-21T11:36:18.188944137Z", "frozenAt": "2026-09-21T11:33:10Z",
"timeoutAt": "2026-09-21T11:46:17Z", "versions": [{"version": 1, …}, {"version": 2, …}, {"version": 3, …}, {"version": 4, …}], …}
| Code | Meaning |
|---|---|
200 |
rolled back and running |
400 |
version negative or not an integer |
404 |
version N not found for sandbox "…" — no such archived version |
409 |
rollback requires a settled snapshot state (frozen/failed/exited), sandbox "…" is running — freeze first |
410 |
disk_lost — never frozen and the disk is gone |
428 |
memory snapshot expired, as on resume |
POST /sandboxes/{id}/clone — fork
A new sandbox from a save point of this one, memory included; the
source is untouched. On a running source without version the live state
is snapshotted first (§5.1). Auth: write on the source; the
caller owns the fork.
| Body | Type · default | Meaning |
|---|---|---|
version |
integer · 0 |
0 = the live state (running source) or the newest save point (frozen source); N = archived version N (see /freeze) |
metadata |
object of strings · {} |
the fork's own metadata |
{"id": "sbx-6d4a…", "templateId": "sbx-44fb…-clone-v2", "state": "running",
"createdAt": "2026-09-21T11:34:54Z", "startedAt": "2026-09-21T11:34:54.897942222Z", "timeoutAt": "2026-09-22T11:34:54Z",
"memoryMB": 4096, "idleTimeout": 600,
"metadata": {"clonedFrom": "sbx-44fb…", "clonedVersion": "2", "purpose": "try-the-migration", "cogenId": "5f63…"}}
A fork starts with a 24-hour deadline; change it with /timeout.
| Code | Meaning |
|---|---|
200 |
the fork is running |
400 |
invalid version: N (must be >= 0) |
402 / 409 |
credits, or the running cap, as on create |
404 |
version N not found for sandbox "…" |
428 |
the source's memory snapshot expired |
503 |
the fork is still starting (Retry-After: 2; the body carries the partial record) |
POST /sandboxes/{id}/timeout — change the lifetime
Auth: write.
| Body | Type · default | Meaning |
|---|---|---|
timeout |
number (seconds) · 0 |
new lifetime from now; 0 (or an empty body) clears the deadline. timeoutMs deprecated |
{"status": "ok"}
| Code | Meaning |
|---|---|
200 |
applied; GET shows the new timeoutAt (absent after 0) |
400 |
invalid timeout: 'soon' (must be a number) |
POST /sandboxes/{id}/reset — hard reset
Kills every process, reboots the kernel, reattaches the disk as it last
was; startCmd runs again. Treat it as a power loss — unflushed data
is lost, so run sync first when the disk state matters, and allow a few
seconds before the first command. Auth: owner. No body. Answers the
running record.
| Code | Meaning |
|---|---|
200 |
reset and running |
404 |
not yours |
409 |
reset requires a settled or running sandbox (mid-freeze → retry) |
503 |
the sandbox's storage is temporarily unavailable (Retry-After) — retry |
GET /sandboxes/{id}/metrics — resource samples
One sample every 5 s while the guest runs. Auth: any user who can see it.
| Query | Type · default | Meaning |
|---|---|---|
start, end |
integer unix seconds · last hour → now | time window; at most 1,200 samples |
[{"timestamp": "2026-09-21T11:32:33Z", "timestampUnix": 1789990353, "cpuCount": 2, "cpuUsedPct": 0.7,
"memUsed": 100118528, "memTotal": 4196507648, "memCache": 81022976, "diskUsed": 2837368832, "diskTotal": 20957446144}]
cpuUsedPct is percent of all cores; memory and disk are bytes. Empty until the first sample lands.
| Code | Meaning |
|---|---|
200 |
the samples (possibly []) |
409 |
the sandbox is killed |
422 |
start / end not integers |
15.4 Sandboxes: commands
A frozen sandbox is resumed first (§4).
POST /sandboxes/{id}/exec — run a command and wait
Auth: write.
| Body | Type · default | Meaning |
|---|---|---|
command |
array of strings · required | argv; use ["/bin/sh", "-c", "…"] for shell syntax |
cwd |
string · the user's home | working directory |
envVars |
object of strings | variables for this command (over the sandbox-wide ones) |
user |
string · the image's user (user) |
root to run as root |
timeout |
number (seconds) · 60 |
capped at 600; at the deadline the command's process gets SIGKILL, its children do not — sh -c "sleep 20" kills the sh, the sleep runs on. Use exec sleep 20 or the binary directly if it must die with the timeout |
{"stdout": "out\n", "stderr": "err\n", "exitCode": 3}
Output is capped at 10 MiB per stream. A non-zero exitCode is a normal
200.
| Code | Meaning |
|---|---|
200 |
the command finished (any exit code) |
400 |
command must be a non-empty list |
408 |
{"error": "exec timed out after Ns", "stage": "timeout", "killed": true} |
409 |
the sandbox is killed / cannot run commands |
POST /sandboxes/{id}/sse/exec — run a command, stream the output
Same body as /exec; the response is text/event-stream. Auth: write.
event: execId
data: {"execId":"gp-stream-1789990365519283415"}
id: 1
event: stdout
data: {"data":"one\n"}
id: 2
event: stderr
data: {"data":"two\n"}
event: exit
data: {"exitCode":2}
An error event ({"error": "…", "stage": "…"}) replaces exit when the
command could not run or timed out ("stage": "timeout").
| Code | Meaning |
|---|---|
200 |
the stream opened; the outcome is in the events |
400 |
command must be a non-empty list |
POST /sandboxes/{id}/commands/start — start a background command
Returns immediately; the process survives client disconnects until kill
or the sandbox ends. Auth: write.
| Body | Type · default | Meaning |
|---|---|---|
command |
array of strings · required | argv |
cwd |
string · the user's home | working directory |
envVars |
object of strings | variables for this process |
user |
string | root or omit |
{"execId": "bg-1789990375798611452", "pid": 75}
execId is what input / close / wait take; pid is what kill and
send-stdin take.
| Code | Meaning |
|---|---|
200 |
started |
400 |
command must be a non-empty list |
GET /sandboxes/{id}/commands — list background commands
Processes started here that have not been waited on yet. Auth: write. No parameters.
[{"execId": "bg-1789990375798611452", "pid": 75, "sandboxId": "sbx-44fb…"}]
| Code | Meaning |
|---|---|
200 |
the array |
POST /sandboxes/{id}/commands/{execId}/input — write to stdin
| Body | Type | Meaning |
|---|---|---|
data |
string · required | bytes to write ("y\n") |
{"written": 2}
| Code | Meaning |
|---|---|
200 |
written |
404 |
unknown execId (already waited on / reaped) |
POST /sandboxes/{id}/commands/{execId}/close — close stdin
Sends EOF. No body.
{"status": "ok"}
| Code | Meaning |
|---|---|
200 |
closed |
404 |
unknown execId |
POST /sandboxes/{id}/commands/{execId}/wait — wait for exit
Blocks until the process exits and removes it from the list. No body.
{"exitCode": 0}
| Code | Meaning |
|---|---|
200 |
exited |
404 |
unknown execId |
POST /sandboxes/{id}/commands/kill — signal a process
| Body | Type · default | Meaning |
|---|---|---|
pid |
integer · required | the process |
signal |
integer · 9 |
e.g. 15 for a graceful stop |
{"status": "ok"}
| Code | Meaning |
|---|---|
200 |
signal sent (the entry stays listed until you wait) |
400 |
pid must be a positive integer |
404 |
no such process |
POST /sandboxes/{id}/commands/send-stdin — write to stdin by pid
| Body | Type | Meaning |
|---|---|---|
pid |
integer · required | the process |
data |
string | bytes to write |
{"status": "ok"}
| Code | Meaning |
|---|---|
200 |
written |
404 |
unknown pid |
15.5 Sandboxes: files
Auth: write for all three. Paths are absolute, or relative to the user's
home (/home/user). A frozen sandbox is resumed first.
POST /sandboxes/{id}/files/stream — write a file
The request body is the file (Content-Type: application/octet-stream
or none). Parent directories are created.
| Query | Type · default | Meaning |
|---|---|---|
path |
string · required | where to write |
user |
string · the image's user | root to write as root |
curl -s -X POST "$API/sandboxes/$SBX/files/stream?path=/home/user/data/in.csv" -H "$H" --data-binary @in.csv
{"written": 20}
| Code | Meaning |
|---|---|
200 |
written; written is the byte count (0 creates an empty file) |
422 |
path missing |
GET /sandboxes/{id}/files/stream — read a file
The response body is the file (application/octet-stream).
| Query | Type · default | Meaning |
|---|---|---|
path |
string · required | the file |
user |
string · the image's user | root to read a root-only file |
| Code | Meaning |
|---|---|
200 |
the bytes (an empty body = an empty file) |
400 |
path is a directory |
403 |
permission denied for that user — retry with user=root. A file inside a directory the user cannot enter (/root/…) reads as 404 instead |
404 |
file not found |
422 |
path missing |
GET /sandboxes/{id}/files/list — list a directory
| Query | Type · default | Meaning |
|---|---|---|
path |
string · / |
the directory |
user |
string | root or omit |
[{"name": "data", "path": "/home/user/data", "isDir": true, "size": 4096},
{"name": "in.txt", "path": "/home/user/in.txt", "isDir": false, "size": 20}]
| Code | Meaning |
|---|---|
200 |
the entries ([] for an empty directory — and for a path that does not exist) |
15.6 Sandboxes: interactive terminal (PTY)
Auth: write. The console's Shell tab is built on these.
POST /sandboxes/{id}/pty — open a terminal
| Body | Type · default | Meaning |
|---|---|---|
command |
array of strings · ["/bin/bash"] |
the shell |
rows, cols |
integers · 24, 80 |
terminal size in characters |
cwd |
string · the user's home | working directory |
envVars |
object of strings | variables for this terminal, over the sandbox-wide ones from create (§7) |
The shell also gets TERM=xterm-256color.
{"execId": "pty-1789990381393509857", "pid": 81}
| Code | Meaning |
|---|---|
200 |
opened |
POST /sandboxes/{id}/pty/{execId}/input — keystrokes
The body is raw bytes (include \n for Enter). Answers {"status": "ok"}.
| Code | Meaning |
|---|---|
200 |
delivered |
404 |
unknown execId |
GET /sandboxes/{id}/pty/{execId}/connect — terminal output
Server-Sent Events: event: data / data: {"data": "…"} with the terminal
bytes, escape sequences included. When the shell ends the stream closes with
event: exit / data: {"reason": "eof", "exitCode": 3} — exitCode is the
shell's status (absent only when the session was reaped before it exited).
| Code | Meaning |
|---|---|
200 |
the stream opened |
404 |
unknown execId |
POST /sandboxes/{id}/pty/{execId}/resize — resize
| Body | Type | Meaning |
|---|---|---|
rows, cols |
integers · required | new size in characters |
Answers {"status": "ok"}.
POST /sandboxes/{id}/pty/{execId}/kill — end the session
No body. Answers {"status": "ok"}.
GET /sandboxes/{id}/ws/pty — WebSocket terminal
Both directions on one connection. Authenticate with ?api_key=gsk-… or
the X-Api-Key header; without a key the handshake is refused (HTTP
403). Every frame is a JSON text message:
| Direction | Frame | Meaning |
|---|---|---|
| you → server, first | {"command": ["/bin/bash"], "cwd": "/home/user", "rows": 24, "cols": 80, "envVars": {"FOO": "bar"}} |
opens the terminal; every field optional (defaults shown, envVars empty). Must arrive within 10 s |
| you → server | {"type": "stdin", "data": "ls\n"} |
keystrokes |
| you → server | {"type": "resize", "rows": 40, "cols": 120} |
new size |
| you → server | {"type": "kill"} |
end the session (the server answers exit and closes) |
| server → you | {"type": "stdout", "data": "…", "seq": 1} |
terminal output, escape sequences included |
| server → you | {"type": "exit", "exitCode": 0} |
the shell ended; the connection closes |
| server → you | {"type": "error", "error": "…"} |
the request could not be served (e.g. a first frame that is not JSON) |
Closing the connection without kill leaves the shell running; end it with
POST /pty/{execId}/kill.
15.7 Sandboxes: sharing
Auth: owner. Same shapes as the template grants.
PUT /sandboxes/{id}/grants/{userId} — share a sandbox
| Body | Type · default | Meaning |
|---|---|---|
perm |
read | write · read |
read = see it; write = also exec, files, freeze / resume, clone |
expires_at |
RFC 3339 timestamp or YYYY-MM-DD · none |
when the grant lapses |
reason |
string | for your records |
{"sandbox_id": "sbx-44fb…", "grantee_id": "0000…abcd", "permission": "read",
"granted_at": "2026-09-21T11:36:25.599604+00:00", "expires_at": null}
| Code | Meaning |
|---|---|
200 |
granted |
400 |
invalid permission: admin; cannot grant to self; invalid expires_at: … |
404 |
not your sandbox |
GET /sandboxes/{id}/grants — list grants
[{"grantee_id": "0000…abcd", "permission": "read", "granted_at": "…", "expires_at": null}]
| Code | Meaning |
|---|---|
200 |
the array |
DELETE /sandboxes/{id}/grants/{userId} — revoke
Answers {"status": "revoked"}.
| Code | Meaning |
|---|---|
200 |
revoked |
16. Response codes
Success
| Code | Returned by | Meaning |
|---|---|---|
200 OK |
almost everything, including create | The body is the record or result. A non-zero exitCode from exec is still a 200. |
202 Accepted |
POST /sandboxes/{id}/freeze |
Only when the snapshot outran the ~50 s wait: body {"state": "freezing"}; poll GET until frozen. |
200 with {"state": "frozen", "status": "already frozen"} |
freeze on a sandbox that is already frozen |
Nothing to do; idempotent. |
200 with the unchanged record |
resume on a running sandbox |
Idempotent no-op (a timeout in the body is still applied). |
200 with [] |
metrics right after create |
No sample yet; the first arrives within about 5 s. |
Deprecated inputs (timeoutMs) succeed but add Deprecation: true and a
Warning: 299 … header.
Errors
Errors are JSON in one of two shapes, chosen by route:
| Shape | Routes |
|---|---|
{"detail": …} — a string for ordinary errors, an object for the billing and plan refusals |
create, get, list, freeze, resume, timeout, clone, reset, metrics, templates, grants; and the sse/exec, commands / commands/start (and the rest of §8.3), files/* and pty routes |
{"error": "…", …} — extra fields vary |
exec and run-code (refusals add sandbox_id, state, suggestion; a timeout adds stage: "timeout", killed, §15.4); rollback (adds reason, recovery, resumable) |
On a record the platform killed, exec answers
409 {"error": "sandbox … is in state \"killed\" and cannot be proxied", "sandbox_id": …, "state": "killed", "suggestion": …},
files/list answers 409 {"detail": "sandbox … is in state \"killed\" and cannot be proxied"},
freeze answers 409 {"detail": "sandbox … is not running (state=killed)"},
rollback answers 410 {"error": …, "reason": "killed", "recovery": ["recreate"], "resumable": false}
and reset answers 410 {"detail": …}.
| Status | detail |
When |
|---|---|---|
400 |
templateId or templateName is required — pick one from GET /templates · invalid timeout: 'abc' (must be a number) · template is not ready (status: building) · command must be a non-empty list · invalid network policy: … · invalid permission: admin · diskSizeGB 25 exceeds the 20 GB limit for user templates on this plan |
bad input — fix the request; nothing was created |
401 (port URL) |
— | a request to a port URL of a sandbox created with allowPublicTraffic: false without the X-Sb-Traffic-Token header (§10.1) |
401 |
authentication required |
no key and no session |
402 |
{"code": "sb_box_insufficient_credits", "balance": "0.00", "message": "…"} |
credits used up — create / resume / clone (billing §1) |
403 |
SB-Box is not enabled for this account |
your account does not have SB-Box access yet |
403 |
{"code": "sb_box_plan_required", "plan": "free", "message": "…", "upgrade": "https://www.genspark.ai/pricing"} |
Free plan — create / run calls need a paid plan; reads, freeze, kill and delete stay open (billing §2) |
404 |
sandbox not found · template not found · no ready template with name '…' |
unknown, killed, or not yours and not shared |
409 |
{"code": "sb_box_concurrency_limit" | "sb_box_spec_limit" | "sb_box_build_concurrency_limit", "plan", "limit", "current" | "requested", "message"} |
a plan cap (billing §2) |
409 |
rollback requires a settled snapshot state (frozen/failed/exited), sandbox "…" is running |
rollback on a running sandbox — freeze first |
409 |
sandbox "…" is killed; … |
any call other than GET / kill on a record the platform killed (§3.0) |
422 |
{"status": -2, "message": "Request parameter validation failed", "errors": [{"loc": ["query", "days"], "msg": "Input should be greater than or equal to 1", …}]} |
a query parameter of the wrong type or range (days outside 1–366, a non-integer start on metrics) |
428 |
{"reason": "memory_expired", "recovery": ["reset"]} (top level, not under detail) |
resume of a sandbox whose memory snapshot was expired by the platform: reset revives it from disk |
499 |
client disconnected |
you hung up before the sandbox was provisioned; nothing was created |
500 / 503 |
string or Internal Server Error |
platform error / region unavailable — retry |
504 |
<phase> timed out; please retry |
the pre-create template lookup exceeded its 20 s budget |
17. Plans, limits and billing
Rates, per-plan limits, running out of credits and GET /limits /
GET /usage: Plans, limits and billing
(markdown: /sb-box/billing.md).
18. SB-Box SDK (coming soon)
An SB-Box SDK is coming soon. Until then the REST API (§15)
and the gsk sb-box CLI (§14) are the
supported ways to use SB-Box.
19. Known limitations
§15 is the contract. Fix rolling out = fixed, reaching each region with its next platform update.
- List rows'
updated_atdoes not move on your own freeze / resume yet (idle freezes and other platform-side transitions do) — fix rolling out. resetis a power-loss reboot: unflushed file data is lost (§15.3 reset).- The list endpoint has no pagination and no metadata filter.
20. Data, regions and security
- Where it runs. A sandbox runs in the region of its template; the public
base template is in
westus2(US West). Templates and sandboxes never move between regions (§13.4). Everything a sandbox stores — its disk, its memory snapshots, its versions — stays in that region. - What is kept, for how long. A running or frozen sandbox keeps its disk
and up to 5 versions (§5). A frozen
sandbox's memory snapshot expires after 14 days untouched; the disk stays
(§4.1).
killdeletes the disk, memory snapshots and every version immediately and removes the record (§3.5); only a sandbox the platform killed stays listable askilledfor a while (§3.4). Deleting a template deletes its image and snapshot. - Who can reach a sandbox. Only the account that created it and the users it explicitly granted access to (§12), with their own tokens. Genspark never reads your data in a sandbox.
- Network defaults. A sandbox has internet access and public port URLs
by default. Restrict egress with
network.allowOut/denyOut/allowInternetAccess, and put a per-sandbox token in front of its port URLs withallowPublicTraffic: false(§10). Do not copy into a sandbox any long-lived credentials you would not put on a shared server. - Isolation. Every sandbox is its own virtual machine with its own kernel; sandboxes never share a kernel with each other or with the host.
21. Acceptable use, abuse reports and feedback
Use of SB-Box is governed by the Genspark Terms of Service and Acceptable Use Policy; sandboxes are not a place for cryptomining, scanning or attacking other systems, hosting content you do not have the rights to, or evading rate limits of other services. Abuse leads to the sandbox being killed and the account's SB-Box access being closed.
- Report abuse coming from a
*.sandbox.*.sspark.aiURL or an SB-Box sandbox: open a ticket in the Help Center at https://www.genspark.ai/helpcenter, or email [email protected], with the URL or sandbox id and the time. - Feedback and bugs: the same Help Center, or the feedback entry in the product. Include the sandbox id, the command and the time (UTC).