boxd Python SDK gives you programmatic access to the full boxd API β create and manage machines, exec commands, stream output, transfer files, manage proxies, templates, disks. It talks to the same public gRPC API as the CLI, so everything you can do from the terminal you can do from Python.
Sync API by default (blocking, context-manager-friendly), with a parallel async API under boxd.aio.
Install
Quick start
Authentication
boxd keys create (CLI), the boxd.sh console UI, or directly via the gRPC API. The SDK itself only issues short-lived JWTs (c.token.create()), not long-lived API keys β see Tokens.
Configuration
By default,Compute connects to production. For custom or self-hosted endpoints, override URLs explicitly:
Environment variables
AllCompute arguments can be supplied via env vars. Constructor args win over env vars.
| Variable | Sets | Default |
|---|---|---|
BOXD_API_KEY | API key (long-lived, recommended) | β |
BOXD_TOKEN | Direct JWT (short-lived) | β |
BOXD_API_URL | gRPC endpoint | http://boxd.sh:9443 |
BOXD_EXCHANGE_URL | Token-exchange URL | https://boxd.sh/api/v1/auth/token |
api_url accepts an optional URL scheme that controls TLS:
api_url value | Transport |
|---|---|
http://host:port | plaintext (scheme stripped before connecting) |
https://host:port | TLS (scheme stripped before connecting) |
bare host:port | TLS, except localhost / 127.* which stay plaintext |
http://boxd.sh:9443 matches production. Self-hosted clusters can pass api_url="http://my-cluster:9443" to opt into plaintext.
Machine lifecycle
stop/start/reboot (cold) and suspend/resume (warm).
Box fields
Box always carries server-returned fields, but which ones are populated depends on how it was obtained:
| Field | create | fork | list | get |
|---|---|---|---|---|
id, name, image, public_ip, status | β | β | β | β |
url, boot_time_ms | β | β | None | None |
forked_from | None | β | None | None |
restart_policy, disk_bytes, auto_suspend_timeout_secs | None | None | None | β |
list / get round-trip, the https://<name>.boxd.sh form is stable, or call box.proxies(). If you need the lifecycle fields off a Box from list / create / fork, re-fetch via c.box.get(box.name).
BoxConfig
create, fork, and template.create_vm all take an optional config:
Exec
Streams: stdout vs stderr
r.stderr / proc.iter_stderr() is populated for non-PTY execs. The
subprocessβs fd 2 is delivered separately from fd 1 β useful when a toolβs
progress lives on stderr while the answer is on stdout (e.g. codex exec,
cargo build).
Under pty=True / interactive=True, the kernel TTY layer merges stderr
into stdout (thatβs how terminals work), so everything arrives on stdout
and the stderr side stays empty. Set pty=False if you need the split.
close_stdin
Pass close_stdin=True (only valid with stream=True on a non-PTY exec)
to have the SDK close the client send half of the bidi stream right after
the command starts. Headless one-shots that read stdin (claude -p, jq,
cat file) see EOF immediately and proceed; without it they hang for
several seconds (or forever) waiting on stdin. Passing it together with
pty=True or interactive=True raises ValueError rather than silently
dropping the flag β a PTY shell needs stdin open for user input.
PTY size + resize
For PTY/interactive execs, passcols and rows to set the initial
terminal geometry, and call proc.resize(cols, rows) to update it
mid-session when the local terminal changes size. This is what makes TUI
apps like claude, vim, htop render at the right width.
cols and rows fall back to the server default of
80Γ24. resize() on a non-PTY exec is a harmless no-op.
File transfer
/home/boxd. Max 100 MB per transfer.
Proxies
Every box is live athttps://name.boxd.sh forwarding to port 8000 by default. Additional subdomain proxies forward to other ports.
Logs
Templates
Reusable image +BoxConfig frozen together.
Disks
Independent persistent volumes you can attach and detach from boxes.Domains
Custom HTTPS domains bound to a box. DNS must already point at the boxd proxy β see HTTPS.Networks
Custom L2 networks for VM-to-VM communication.Tokens
Issue scoped JWTs for delegated access. The raw token string is only returned at creation β store it then.Identity
Errors
| Class | gRPC status |
|---|---|
AuthenticationError | UNAUTHENTICATED, PERMISSION_DENIED |
NotFoundError | NOT_FOUND |
QuotaExceededError | RESOURCE_EXHAUSTED |
InvalidArgumentError | INVALID_ARGUMENT, ALREADY_EXISTS |
TimeoutError | DEADLINE_EXCEEDED |
ConnectionError | UNAVAILABLE |
InternalError | INTERNAL, UNKNOWN |
grpc_code (numeric gRPC status β see grpc.StatusCode) for finer-grained handling:
Update notifications
Every gRPC response carries anx-boxd-py-sdk-latest header set by the boxd proxy. The SDKβs interceptor compares it to the installed version and prints a one-time sys.stderr line if a newer release is available:
.devN suffixes).
Sync vs async
The defaultboxd.Compute is the sync API β fully blocking, safe for scripts, REPLs, notebooks, Django views, anywhere you donβt already have an event loop. It wraps the async implementation behind a dedicated background loop, so you donβt pay for asyncio setup yourself.
boxd.aio.Compute is the async API β use it from inside an existing event loop (FastAPI, asyncio scripts, Quart, anyio):
from boxd import Compute | from boxd.aio import Compute | |
|---|---|---|
| Call style | c.box.create(...) | await c.box.create(...) |
| Context manager | with | async with |
| Best for | scripts, CLIs, notebooks | FastAPI, async workers, concurrent fan-out |
boxd unless you already have an event loop.
Reference
CLI
Same API, accessed from the terminal. Useful for one-offs and shell scripting.
Suspend & resume
box.suspend() / box.resume() and how auto-suspend interacts with them.