Portal
Documentation: all sections

Sandbox compute

POST /v1/sandboxes runs code in an isolated sandbox, bills it in vCPU-seconds and GB-seconds of memory, and returns a signed receipt. Same API key as inference, same credit balance, same spend caps.

Agents need somewhere to run code far more often than they need a GPU. What Sable adds on top of the execution itself is the control plane: a hard spend ceiling, a key scope, a retry that cannot execute or bill twice, and a per-run receipt you can hand to someone else as evidence.

Run some code

curl https://api.buildsable.com/v1/sandboxes \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-d '{
  "language": "python",
  "code": "print(sum(range(10)))"
}'

The response:

{
  "id": "sbx_7f3c…",
  "object": "sandbox.run",
  "status": "succeeded",
  "exit_code": 0,
  "stdout": "45\n",
  "stderr": "",
  "truncated": false,
  "duration_ms": 412,
  "vcpu": 1,
  "vcpu_seconds": 1,
  "cost_micro_usd": 17,
  "receipt": { "receipt": "eyJ2IjoyLC…", "signature": "0x…", "signer": "0x…" },
  "replayed": false
}

Each output stream is capped at 256 KiB, cut on a UTF-8 character boundary; when either stream is cut, truncated is true.

Request fields

FieldDefaultNotes
coderequiredThe program to run. Fed to the runtime over stdin.
languagepythonpython, node, or bash.
imageper languageExplicit runtime image. Overrides the language default.
timeout_secs30Clamped to the deployment maximum: 4 hours on production. For long jobs use stream: true so output (and liveness) arrives as it happens.
vcpu1Clamped to the deployment maximum.
mem_mb512Clamped to the deployment maximum. Memory is billed (see below), so ask for what the run needs, not the ceiling.
envnoneEnvironment variables. Request-scoped, never stored.
networkfalseNetwork egress. Off by default: generated code doesn't get the internet unless you ask. On a deployment whose sandbox backend does not permit egress, network: true is refused rather than silently ignored.

Streaming output

Pass "stream": true and the response becomes an SSE stream: sable.stdout and sable.stderr events carry output the moment your program prints it, and a terminal sable.result event carries the exact same response body the buffered call returns, signed receipt included. Billing is identical either way, and a client that disconnects mid-stream is still billed for the run (the slot was held and the run completes on the node).

curl -N https://api.buildsable.com/v1/sandboxes \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-d '{
  "stream": true,
  "language": "python",
  "code": "import time\nfor i in range(3):\n    print(i, flush=True)\n    time.sleep(1)"
}'

# event: sable.stdout   data: {"text":"0\n"}     ← arrives at t≈0s
# event: sable.stdout   data: {"text":"1\n"}     ← t≈1s
# event: sable.stdout   data: {"text":"2\n"}     ← t≈2s
# event: sable.result   data: { …full response with receipt… }

Output caps apply identically (256 KiB per stream); chunks past the cap are dropped from the stream and truncated is true on the result. On a node that predates streaming, the gateway falls back to a buffered run and the terminal event still carries the full output.

Both SDKs consume this for you: sable.runCodeStream(req) (TypeScript, an async iterator of stdout/stderr/result events) and sable.run_code_stream(...) (Python, a generator yielding ("stdout", text) / ("stderr", text) then ("result", run)).

Retrying safely

A sandbox call executes code and bills for it, so a blind retry after a dropped connection is a second execution and a second charge. Send an optional Idempotency-Key header and the retry is safe: at most one execution per key, per account. The key is claimed before the code runs, so two concurrent retries race to a single winner rather than both executing.

curl https://api.buildsable.com/v1/sandboxes \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-H 'Idempotency-Key: 6f1c2a8e-4b77-4a1d-9d0e-2b7d5c9a1f30' \
-d '{
  "language": "python",
  "code": "print(sum(range(10)))"
}'

The key is any printable-ASCII string up to 200 characters: a UUID or a hash of the job is the usual choice. A non-ASCII or over-length key is a 400 rather than a silent no-op, because a key that quietly does nothing is worse than no key at all. Keys are scoped to your account, and a request without the header behaves exactly as it did before.

SituationWhat you get
First request with a keyThe run executes normally. "replayed": false.
Retry after that run finishedThe original signed receipt, replayed byte-identical, with "replayed": true. The code is not executed again and nothing is billed again.
Retry while the run is still in flight409 with type: conflict. The only alternatives would be to stall or to run your code twice. Retry once it finishes.
Retry after the run failed (502 backend or capacity unavailable)The key was released, so this genuinely re-runs. Nothing was billed for the failed attempt.
Key over 200 chars, or containing non-ASCII400, before anything runs.

A replay returns empty output

Sandbox stdout and stderr are never written to disk or a log line (see Privacy below, and the privacy contract), so there is nothing to replay them from. A replayed response carries the original receipt and the run metadata, and both output streams come back as "":

{
  "id": "sbx_7f3c…",
  "object": "sandbox.run",
  "status": "succeeded",
  "exit_code": 0,
  "stdout": "",
  "stderr": "",
  "truncated": false,
  "duration_ms": 412,
  "vcpu": 1,
  "vcpu_seconds": 1,
  "cost_micro_usd": 17,
  "receipt": { "receipt": "eyJ2IjoyLC…", "signature": "0x…", "signer": "0x…" },
  "replayed": true
}

Keep the output from the first response. Idempotency protects your credit and your side effects; it cannot hand you back an answer the gateway deliberately never kept. Always branch on replayed before treating stdout as the result of the run.

What it costs

Billing has two dimensions, both wall-clock, both rounded up, minimum one second (a partial second still occupies a slot):

cost_micro_usd on the response is the total charged amount across both dimensions, and it also appears in the receipt and in GET /v1/usage.

Two things you are not billed for:

A run that times out is billed: it held the slot for the full window.

While a run is in flight it reserves its worst case against your prepaid balance (every second of its timeout_secs, on every vCPU and every MB of memory it asked for), and the reservation is released the moment the run settles, when you are charged the actual usage instead. A generous timeout_secs with a large mem_mb therefore reserves a lot of credit even if the run finishes in a second. Spendable credit is your balance minus what in-flight requests have reserved, so a 402 can mean credit is reserved by requests already in flight rather than that your balance is empty; the error message says which. That state is temporary: retry once those runs settle. See Cost & metering.

Concurrency

An account can hold a bounded number of sandbox runs in flight at once (default 8). A request over the cap returns 429 with a Retry-After header: nothing is executed and nothing is billed. Queue on your side and retry, or finish runs faster with a tighter timeout_secs.

Spend controls

Everything that governs an inference key governs sandboxes too (see API key controls):

Privacy

Your code, its stdout, and its stderr cross the gateway in-frame, are returned to you, and are never written to disk or a log line. The sandboxes table has no column that could hold them.

The one content-derived value that persists is content_fp: a sha256 prefix of the submitted payload, the same fingerprint scheme the inference receipts use. It proves which payload ran without revealing what it was.

Receipts

Sandbox receipts are v: 2 and describe compute rather than tokens:

{
  "v": 2,
  "kind": "sandbox",
  "resource": "python:3.12-slim",
  "unit": "vcpu_seconds",
  "quantity": 1,
  "cost_micro_usd": 17,
  "status": "succeeded",
  "exit_code": 0,
  "content_fingerprint": "3954a524ccabab70",
  "logging": "metadata-only"
}

They are signed by the same key as inference receipts and verify through the same POST /v1/receipts/verify. A distinct version rather than a reinterpreted inference receipt, because vCPU-seconds are not tokens and a receipt shouldn't lie about what it measured.

Isolation, honestly

Production sandboxes run on Sable-operated infrastructure. A deployment runs one of two backends:

If a run is refused with 501, sandbox compute isn't enabled on that deployment.