Skip to main content

sandboxer-sdk — Python SDK

Python client for Sandboxer: persistent cloud sandboxes (Ubuntu microVMs with root, Docker, SSH, HTTPS port hosting, stop/resume, forks, snapshots, a streamed desktop and integrated coding agents).

  • High-level API — Sandboxer / Sandbox (sync) and AsyncSandboxer / AsyncSandbox (asyncio).
  • Low-level API — SandboxerApi / AsyncSandboxerApi: one typed method for every operation of the public API (/api/v1), wire-compatible with boat's API v1 so boat-sdk code ports by renaming imports.
  • Typed pydantic models, envelope-aware errors, automatic retries with jittered backoff, automatic Idempotency-Key on create/fork, NDJSON command streaming, event streaming (polling and SSE) and webhook signature verification.

Requires Python 3.10+.

pip install sandboxer-sdk     # imported as `import sandboxer`

Configure

Option Default
api_key SANDBOXER_API_KEY, then SANDBOXER_TOKEN (the per-sandbox key injected inside sandboxes)
base_url SANDBOXER_API_URL, else https://sandboxer.com/api/v1
org SANDBOXER_ORG; sent as X-Sandboxer-Org (billing wallet)
timeout 60 seconds (None disables it)
max_retries 3
http_client your own httpx.Client / httpx.AsyncClient (not closed by the SDK)
default_headers extra headers for every request
from sandboxer import Sandboxer

client = Sandboxer()                       # reads SANDBOXER_API_KEY
client = Sandboxer(api_key="sbx_live_...", org="team_acme", timeout=30)

Keep API keys, secrets, SSH keys and desktop/hosted URLs (they can carry access tokens) out of your logs.

Quickstart

from sandboxer import Sandboxer

with Sandboxer() as client:
    sandbox = client.create(type="small", ttl_seconds=1800)   # waits until it is ready

    result = sandbox.exec("python3 --version")
    print(result.exit_code, result.stdout)

    sandbox.files.write("app/main.py", "print('hi')\n")
    print(sandbox.files.read("app/main.py"))

    sandbox.start("python3 -m http.server 8000", cwd="app")      # detached
    print(sandbox.host(8000, public=True).url)                   # https://<slug>-8000.box.sandboxer.com

    sandbox.stop()                                                # archive (resume later)

Sandboxer.create(...), Sandboxer.get(...) and Sandboxer.list(...) also work on the class itself, using a default client configured from the environment:

sandbox = Sandboxer.create(ttl_seconds=600)

Create options

client.create(type=, ttl_seconds=, env=, no_env=, from_=, setup_script=, environment=, snapshots=, monitoring=, fail_fast=, org=, name=, idempotency_key=, wait=True, wait_for_setup=False, timeout=300, on_exit=None)

  • ttl_seconds=None disables auto-stop (omitting it uses the server default of one hour).
  • from_="web-stack" starts from a named snapshot (or a bsh_... share code).
  • no_env=True withholds every account secret — use it for sandboxes you hand to your own users.
  • wait_for_setup=True also waits for setup_script and raises if it failed.
  • Waiting raises SandboxStateError if the sandbox ends in error/cancelled, WaitTimeoutError on timeout.

Commands

r = sandbox.exec(["ls", "-la", "my dir"], cwd="project", env={"DEBUG": "1"}, timeout_seconds=120)
r.stdout, r.stderr, r.exit_code, r.success, r.timed_out

sandbox.exec("make test", check=True)        # raises CommandError on a non-zero exit

Commands run through the shell; a list is quoted for you. The API has no env field for commands, so env is exported in front of the command line.

Streaming output

stream = sandbox.exec_stream("npm install")
for frame in stream:                          # started, stdout/stderr chunks, then exit or error
    if frame.type in ("stdout", "stderr"):
        print(frame.data, end="")
result = stream.wait()                        # CommandResult with the collected output

Detached commands

server = sandbox.start("node server.js", cwd="app")
server.process_id, server.pid, server.command_id

server.status()                               # running?, exit code, log tails
for frame in server.attach():                 # live output; reconnects transparently
    print(frame.type, frame.data)
server.kill()                                 # SIGTERM, then SIGKILL after 5 s
server.wait()                                 # follow to the end -> CommandResult
server.logs()                                 # full stored stdout from the command history

sandbox.command_status(42); sandbox.attach(42); sandbox.kill(42)   # by process id

Files

sandbox.files.write("notes.txt", "hello\n")          # text
sandbox.files.write("logo.png", png_bytes)           # bytes (sent base64)
sandbox.files.read("notes.txt")                      # -> str   (up to 5 MiB)
sandbox.files.read_bytes("logo.png")                 # -> bytes (up to 5 MiB)
sandbox.files.download("build/app.tar.gz")           # -> bytes (up to 50 MiB)

Paths are absolute or relative to /home/user, and must resolve under /home/user or /tmp.

Hosting, SSH and desktop

sandbox.host(3000)                          # private URL with a _token query parameter
sandbox.host(3000, public=True)             # or private=False
sandbox.host(5173, localhost=True)          # service bound to 127.0.0.1 only
sandbox.ssh_key(Path.home().joinpath(".ssh/id_ed25519.pub").read_text())  # then ssh user@<machine_ip>
sandbox.desktop(public_access=True).desktop_url         # waits while VNC is provisioning

Lifecycle

sandbox.stop()                        # waits until archived (wait=False to return at once)
sandbox.resume(type="large")          # waits until ready again
child = sandbox.fork()                # new Sandbox from the current state; the source is untouched
sandbox.snapshot("web-stack")         # named snapshot, waits until ready -> deploy with create(from_="web-stack")
sandbox.extend(7200)                  # auto-stop in 2 h from now (None disables auto-stop)
sandbox.update(name="demo", subdomain="acme-demo")
sandbox.usage(); sandbox.processes(); sandbox.history(); sandbox.snapshots(); sandbox.latest_snapshot()
sandbox.delete()                      # PERMANENT: deletes the sandbox and its snapshots

for sb in client.list(state=["ready", "idle"]):
    print(sb.id, sb.state)

Context managers

with client.create(on_exit="delete") as sandbox:   # or on_exit="stop"; default None leaves it running
    sandbox.exec("pytest", check=True)

on_exit can also be set later: sandbox.on_exit = "stop".

Agents: prompts and events

run = sandbox.prompt("Run the tests and fix what fails.", provider="codex", model="gpt-5.4")
for event in run.events():              # this run's events, ending when it is done
    if event.type == "response" and event.data.get("content"):
        print(event.data["content"])
run.wait()                              # PromptRun: finished / failed / interrupted
print(run.text())                       # final response text
run.steer("Skip the integration tests.")
run.interrupt()

for event in sandbox.events(type="prompt,response"):   # every conversation, follows new events
    ...
sandbox.conversations(); sandbox.steer("..."); sandbox.interrupt(conversation="...")

sandbox.events(mode=...): "poll" long-polls GET /events?sort=asc with the page cursor, "sse" uses text/event-stream (reconnecting with Last-Event-ID), "auto" (default) tries SSE and falls back to polling. Repeated pages are de-duplicated, while an event whose payload changed (a streaming partial becoming final) is yielded again.

Async

Everything above exists with async/await on AsyncSandboxer / AsyncSandbox:

import asyncio
from sandboxer import AsyncSandboxer

async def main() -> None:
    async with AsyncSandboxer() as client:
        async with await client.create(on_exit="delete") as sandbox:
            print((await sandbox.exec("uname -a")).stdout)
            async for frame in sandbox.exec_stream("ls -R /etc"):
                ...

asyncio.run(main())

Async versions of the helper functions are in sandboxer.aio.

Low-level client

client.api is a SandboxerApi with one method per API operation (snake_case operation ids):

from sandboxer import models

api = client.api
api.me(); api.limits(); api.sandboxes(state="ready,idle")
created = api.create(ttl_seconds=None, no_env=True)                 # keyword body fields...
api.update(created.sandbox.id, models.UpdateSandboxRequest(name="x"))  # ...or a body model / dict
api.events(sandbox_id, sort="asc", limit=50, type="prompt,response")
for frame in api.command_stream(sandbox_id, command="make"): ...
api.request("GET", "/provider-models")                             # escape hatch
  • Keyword arguments left out are not sent; None is sent as JSON null (ttl_seconds=None disables auto-stop).
  • request_options={"timeout": 5, "max_retries": 0, "headers": {...}, "org": "team_x", "idempotency_key": "..."} works on every method.
  • Deletes fill X-Sandboxer-Confirm-Delete with the target id for you (override with confirm_delete=).
  • Responses are pydantic models with snake_case attributes (sandbox.memory_gb); unknown fields are kept (model_extra) and unexpected shapes are logged instead of raising.

Errors, retries and idempotency

Every failure raises SandboxerError (subclasses: BadRequestError, AuthenticationError, PaymentRequiredError, PermissionDeniedError, NotFoundError, ConflictError, RateLimitError, InternalServerError, APIConnectionError, APITimeoutError, plus WaitTimeoutError, SandboxStateError, CommandError, WebhookVerificationError).

from sandboxer import SandboxerError

try:
    sandbox.exec("true")
except SandboxerError as e:
    print(e.status, e.code, e.message, e.details, e.request_id, e.retryable)

Automatic retries (default 3, jittered exponential backoff, Retry-After honored):

Request Retried on
GET network errors, timeouts, 5xx, 429 with Retry-After, sandbox_starting, retryable sandbox_direct_failed
POST /commands only sandbox_starting, sandbox_direct_failed + sandbox_restoring (the command provably did not run) and 429 with Retry-After — never network errors or 5xx, since the command may already be running
create / fork network errors, timeouts, 5xx, 429 with Retry-After, 409 idempotency_in_progress — safe because each call carries an Idempotency-Key (a UUID, reused across retries; pass idempotency_key= to choose it)
other writes 429 with Retry-After, sandbox_starting, retryable sandbox_direct_failed

Webhooks

Deliveries carry Sandboxer-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. Verify the raw bytes before parsing:

from sandboxer import construct_event, verify_webhook, WebhookVerificationError

# FastAPI
@app.post("/hooks/sandbox")
async def hook(request: Request):
    raw = await request.body()
    try:
        event = construct_event(raw, request.headers, os.environ["SANDBOXER_WEBHOOK_SECRET"])
    except WebhookVerificationError:
        raise HTTPException(400)
    if event.type == "sandbox.ready":
        ...
    return {"ok": True}

# Flask
@app.post("/hooks/sandbox")
def hook():
    if not verify_webhook(request.get_data(), request.headers.get("Sandboxer-Signature"), SECRET):
        abort(400)
    ...
  • tolerance_seconds (default 300) rejects stale timestamps; pass a list of secrets while rotating.
  • verify_webhook_headers(raw, headers, secret) / construct_event(raw, headers, secret) also accept the X-Sandboxer-Signature / X-Sandboxer-Delivery / X-Sandboxer-Timestamp header set.
  • Deliveries are at-least-once and may arrive out of order: de-duplicate on event.id.

Porting from boat-sdk

boat-sdk sandboxer
from boat_sdk.api.boat_api import BoatApi from sandboxer import SandboxerApi (or Sandboxer().api)
Configuration(host=..., access_token=...) Sandboxer(base_url=..., api_key=...)
BOAT_API_KEY, X-Boat-Org SANDBOXER_API_KEY, X-Sandboxer-Org (org=)
boat_sdk.models.CreateSandboxRequest(var_from=...) sandboxer.models.CreateSandboxRequest(from_=...)
sandbox.create(CreateSandboxRequest(ttl_seconds=1800)) api.create(CreateSandboxRequest(ttl_seconds=1800)) or api.create(ttl_seconds=1800)
sandbox.get(id), update, stop, resume, fork, delete_sandbox, prompt, events, read_file, write_file, command, artifact, desktop, ssh_key, snapshot methods ... same names on SandboxerApi
ApiException (.status, .body) SandboxerError (.status, .code, .message, .details, .request_id)
wait_until_ready, wait_until_idle, wait_for_desktop, wait_for_prompt, wait_for_prompt_done, stream_events, stream_prompt, stop_and_remove, read_text, write_text, exec_command same names in sandboxer (async versions in sandboxer.aio)

Development

python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python scripts/generate.py     # regenerate models + low-level client from ../../api/openapi.yaml
.venv/bin/python scripts/unasync.py      # regenerate the sync modules from the async sources
.venv/bin/pytest -q && .venv/bin/ruff check src tests && .venv/bin/mypy --strict src

models.py, _generated_api.py and the *_sync.py modules are generated: edit the generator or the async sources.

Support

Questions and bug reports: support@sandboxer.com. Full docs: sandboxer.com/docs/sdk-python.

License

MIT

Metadata

Release files for sandboxer-sdk 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sandboxer-sdk 0.1.1
File Size Uploaded
sandboxer_sdk-0.1.1.tar.gz 130.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sandboxer-sdk 0.1.1
File Interpreter ABI Platform
sandboxer_sdk-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 252.5 kB

Release files / sandboxer_sdk-0.1.1.tar.gz

Download URL sandboxer_sdk-0.1.1.tar.gz
Size 130.7 kB
Tags Source
SHA-256 checksum
How to use checksums
574963be45db7f20b7535c0b660a7b13dd91666a571749315507f342b7646d59
BLAKE2b-256 checksum
How to use checksums
0b30405a76976f4a92a8da7451e39b76d652b409e287e454fc0c61a6561bce69
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / sandboxer_sdk-0.1.1-py3-none-any.whl

Download URL sandboxer_sdk-0.1.1-py3-none-any.whl
Size 121.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ca9100057adb6b21ece2adf9559703278542f1299cb29a9bfad9b07ef6594e0e
BLAKE2b-256 checksum
How to use checksums
f3a643e046a2632a78a41cac4df73e6e50a04c74a763b89bba890fff6b58ffb9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page