Portal
Documentation: all sections

Sandbox sessions

POST /v1/sandboxes runs code in a fresh container and throws it away. That is the right shape for a one-shot job and the wrong shape for the loop a coding agent actually runs: write a file, run the tests, read the failure, edit, run again. Doing that against one-shot runs means re-uploading the whole working state on every call, and paying to rebuild it each time.

A session is a long-lived container with a writable /workspace that survives between commands. Same API key, same balance, same spend caps, same signed receipt per command — the only thing that changed is that the filesystem was already there.

Open a workspace

curl https://api.buildsable.com/v1/sandboxes/sessions \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-H 'idempotency-key: agent-run-42' \
-d '{
  "language": "python",
  "vcpu": 1,
  "mem_mb": 1024,
  "idle_timeout_secs": 300
}'

Pass an Idempotency-Key header on the create. A retry under the same key returns the session you already have, rather than opening a second container you would keep paying for and never know about.

Running commands

POST /v1/sandboxes/sessions/{id}/exec takes either code (with an optional language) or an explicit argv such as ["pytest", "-q"], which runs with /workspace as the working directory. It returns the same fields a one-shot run returns, plus a signed receipt whose payload carries session_id.

Add "stream": true to get stdout and stderr over SSE as they are produced — identical framing to streaming sandbox runs, with a terminal sable.result event carrying the full response. For a command that takes minutes, the stream is also the liveness signal.

Files

MethodPathWhat it does
PUT/v1/sandboxes/sessions/{id}/files/{path}Write one file (raw body, ≤ 8 MiB).
GET/v1/sandboxes/sessions/{id}/files/{path}Read one file back, as raw bytes.
GET/v1/sandboxes/sessions/{id}/files?path=List a directory.

Paths are workspace-relative. A leading / or any .. segment is rejected at both ends, by the gateway and again by the node.

These are relays, not storage. The bytes cross the gateway in-frame and are written nowhere: there is no file column, and there is no way to ask Sable for a file you did not keep. The workspace lives on the node for as long as the session does, and goes with it.

Snapshots

POST /v1/sandboxes/sessions/{id}/snapshot captures the workspace and returns an id you can hand to a later create as snapshot_id — a prepared environment (dependencies installed, repository cloned) reused instead of rebuilt.

The archive stays on the node that took it. Sable records the id, the byte count and an expiry; it never holds the contents. Two consequences worth knowing: a session started from a snapshot is pinned to that node, and snapshots expire — treat one as a warm cache, not as durable storage.

GET /v1/sandboxes/snapshots lists yours, DELETE /v1/sandboxes/snapshots/{id} removes one.

Network egress

Egress is off by default, exactly as for a one-shot run. network: true opens it; egress_allow narrows it to named hosts:

{
  "language": "python",
  "network": true,
  "egress_allow": ["pypi.org", "*.pythonhosted.org"]
}

An entry is either an exact hostname or a *.suffix wildcard, which also matches the suffix itself. With a non-empty allowlist the node places the workspace on an internal network with no default route and no DNS of its own, alongside a forward proxy that is its only way out, and injects HTTP_PROXY / HTTPS_PROXY so ordinary clients use it. A request to anything not on the list is refused at the proxy, and so is any host that resolves to a private or loopback address.

Be precise about what this is and is not. It is a hostname allowlist, not traffic inspection: TLS is not intercepted. Sable learns which hosts a workspace was permitted to reach and nothing about what travelled inside those connections. Values you put in env stay between you and your own code. If you need a stricter boundary than "these hostnames", this is not it.

What it costs

Two meters, because there are two different things being consumed.

So an idle workspace is cheap but never free, and that is deliberate: capacity you are holding is capacity nobody else can be sold. A session reserves its next window of alive time as a credit hold and rolls it forward; if your balance can no longer cover the window, the session is stopped rather than allowed to accrue a bill you cannot pay.

When a session ends

A workspace stops when you DELETE it, when it has been idle past its idle_timeout_secs (default 300 s), when it reaches max_lifetime_secs (4 hours by default), when its node becomes unreachable, or when credit runs out. A sandbox_session_stopped webhook fires with the reason.

A stopped session is gone, not paused: the container and everything in it no longer exist, and a command against it answers 409 rather than quietly starting a fresh one. Snapshot first if you want the state back. Stopping is idempotent — a second DELETE bills nothing further.

Endpoints

MethodPathNotes
POST/v1/sandboxes/sessionsOpen a workspace. Accepts Idempotency-Key.
GET/v1/sandboxes/sessionsYour sessions, newest first (metadata only).
GET/v1/sandboxes/sessions/{id}One session: status, alive seconds, exec count, cost.
POST/v1/sandboxes/sessions/{id}/execRun a command. stream:true for SSE.
PUT/v1/sandboxes/sessions/{id}/files/{path}Upload one file (≤ 8 MiB).
GET/v1/sandboxes/sessions/{id}/files/{path}Download one file (≤ 8 MiB).
GET/v1/sandboxes/sessions/{id}/filesList a directory (?path=).
POST/v1/sandboxes/sessions/{id}/snapshotSnapshot the workspace.
DELETE/v1/sandboxes/sessions/{id}Stop it.
GET/v1/sandboxes/snapshotsYour snapshots.
GET/v1/sandboxes/snapshots/{id}One snapshot's metadata.
DELETE/v1/sandboxes/snapshots/{id}Delete a snapshot.

Sessions need the http-runner backend — the workspace, its files, its snapshots and its egress proxy all live on the node. A deployment running the local Docker backend answers 501 and points you at POST /v1/sandboxes.

Privacy

Unchanged from the privacy contract. The code a command submits, its environment, its output and every file in the workspace cross the gateway in-frame and are never written to a database row or a log line. The session table holds ids, sizes, timings, counts and money; there is no column that could hold your work. Snapshots are the one thing kept on disk — on the node, by your explicit request, under a TTL — and Sable stores an id and a byte count, never the contents.