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) andAsyncSandboxer/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-Keyon 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=Nonedisables auto-stop (omitting it uses the server default of one hour).from_="web-stack"starts from a named snapshot (or absh_...share code).no_env=Truewithholds every account secret — use it for sandboxes you hand to your own users.wait_for_setup=Truealso waits forsetup_scriptand raises if it failed.- Waiting raises
SandboxStateErrorif the sandbox ends inerror/cancelled,WaitTimeoutErroron 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;
Noneis sent as JSONnull(ttl_seconds=Nonedisables auto-stop). request_options={"timeout": 5, "max_retries": 0, "headers": {...}, "org": "team_x", "idempotency_key": "..."}works on every method.- Deletes fill
X-Sandboxer-Confirm-Deletewith the target id for you (override withconfirm_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 theX-Sandboxer-Signature/X-Sandboxer-Delivery/X-Sandboxer-Timestampheader 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.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sandboxer_sdk-0.1.0.tar.gz | 130.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sandboxer_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 252.4 kB
Release files / sandboxer_sdk-0.1.0.tar.gz
| Download URL | sandboxer_sdk-0.1.0.tar.gz |
|---|---|
| Size | 130.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
97484eecf02c8aee33476254e810241989da17ee375ead190a689d0b00edf242
|
|
BLAKE2b-256 checksum How to use checksums |
270f31057eba601a1df126758fb2f5e6e20c4d84a61a90648797ef63352a45ec
|
| 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 15, 2026.
Transparency logRelease files / sandboxer_sdk-0.1.0-py3-none-any.whl
| Download URL | sandboxer_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 121.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e853e97534a1beb38918ce1ddad972f55ffcf9b1d3ed5b26ac31bf91d96f6d13
|
|
BLAKE2b-256 checksum How to use checksums |
3b88f07eb92362cb600e48d2b97194a51d74b09d448dcf31731702c994de475d
|
| 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 15, 2026.
Transparency log