boxd package gives you programmatic access to the full boxd API — create and manage machines, run commands in them, stream output, move files, and manage proxies, snapshots, disks, env vars and secrets. It talks to the same API as the CLI, so everything you can do from the terminal you can do from Python.
Requires Python 3.10+.
Install
Quick start
Machine is a record of fields, not a handle, and every operation takes the machine id (or name) as its first argument: boxd.<resource>.<verb>(id, ...).
There are eight namespaces:
The client
base_url selects the cluster.
All arguments are keyword-only.
base_url accepts an optional scheme that controls TLS:
Failed connections are retried up to
max_retries times with exponential backoff. Timeouts are never retried — the request may already have been applied — and authentication failures are never retried either.
The client holds a connection, so keep one around rather than making a new one per call. Close it when you’re done, or use it as a context manager:
Sync and async
Boxd and AsyncBoxd are two top-level classes with the same surface — same namespaces, same method names, same arguments, same return types. Switching is await and an import, not a rewrite.
AsyncBoxd when you already have an event loop (FastAPI, asyncio scripts, anyio). Use Boxd everywhere else — scripts, notebooks, Django views.
Authentication
- an explicit
token - an explicit
api_key BOXD_TOKENBOXD_API_KEY- automatic, inside a boxd machine (below)
AuthenticationError.
Mint a key with boxd auth keys create NAME (see the CLI), in the console, or with boxd.api_keys.create(). An API key is exchanged for a short-lived session and kept fresh for you. Revoking a key takes effect immediately, so if a key is revoked mid-run the next call raises AuthenticationError rather than retrying.
Inside a machine
Inside a boxd machine,Boxd() authenticates automatically — no API key, no configuration:
base_url or credential still wins.
Machines
list() returns a plain list. Pass org="acme" for one organization’s machines, or all_contexts=True for everything you can reach.
State:
stop/start (cold) and pause/resume (warm).
Everything else:
create and fork return once the machine is scheduled, not once it is usable. Call wait_until_ready before doing anything that depends on it running — especially before forking it again. It polls for up to 90 seconds by default (timeout=, poll_interval=, both in seconds).
Creating
create() boots the default image at the default size. fork(source, name) takes the same keywords, and anything you leave out is inherited from the source.
Proxies and disk mounts are set up at create time with two small models:
A snapshot restores the machine that was captured, so
create(from_snapshot=...) takes only name, org and the sizing keywords. Passing image, env, cmd, restart_policy or shared alongside it raises ValueError rather than silently ignoring them.Machine.
The Machine record
org is the organization the machine belongs to and is billed to; shared says whether your teammates can see it. A private machine can still be org-billed, so org set with shared=False is normal, not a contradiction — see Organizations.
source.id may not resolve if the machine or snapshot it points at was since deleted. A lookup that finds nothing is normal.
Models are pydantic, so machine.model_dump() gives you a plain dict. An unrecognised status is passed through rather than rejected, so a machine in a newly added state still prints.
Exec
One-shot exec collects the output:command is shell-quoted for you; a string is passed through as written. timeout is in seconds and gives up on the call — the remote process may keep running. exec also takes tty, cols and rows: under a PTY the terminal merges stderr into stdout, so stderr comes back empty and everything lands in stdout.
For anything interactive, stream_exec gives you a live session — the one handle in the SDK, because a bidirectional stream really is stateful:
iter_chunks() yields OutputChunk(data, is_stderr) when you need the two streams apart. Under tty=True the terminal merges them, so everything arrives as stdout; set tty=False if you need the split. exit_code is None until the stream is exhausted.
Set the terminal size with cols/rows, and call stream.resize(cols, rows) when the local terminal changes size:
cols/rows fall back to 80×24. resize() on a non-PTY exec is a harmless no-op.
Headless one-shots that read stdin (jq, cat, claude -p) hang waiting for input. Pass close_stdin=True to send end-of-input immediately — or call write_eof() yourself. Combining it with tty=True raises ValueError, since a shell needs stdin open.
Logs
follow=True keeps the stream open for new output.
Files
upload takes str or bytes and returns the number of bytes written; large uploads are chunked for you. Paths inside the machine are absolute, or relative to /home/boxd.
Ports
Raw TCP/UDP forwards on a public address — see Port forwarding. Max 3 per machine."both" shares one public port across TCP and UDP.
On
AsyncBoxd, ports.list() takes no machine argument — it returns every forward you own. Filter on .machine_id or .machine_name.Proxies
HTTPS routes into a machine — see Proxies. The machine argument takes an id or a name, like everywhere else.name and port are both required on create — a named route is always pinned to a port. "auto" is only accepted for a machine’s default route, which is what set_port addresses when you leave name off.
Checkpoints
Per-machine save points, restored in place — see Checkpoints. They are deleted with the machine.create returns while the capture is still "pending".
Snapshots
Reusable, named captures — see Snapshots. Boot one withmachines.create(from_snapshot=...).
Snapshot carries created_at (the first capture), updated_at (the most recent one), version, status, size_bytes, vcpu, memory_bytes and use_count. get, list and delete take an optional org=.
Disks
size takes a human string ("10G") or a byte count. A disk is always created writable; read-only is chosen per attachment. A disk can be attached to one machine at a time. status is "creating", "ready" or "destroyed" — it can only be attached once it is "ready". list() also returns each disk’s current attachments.
You can mount a disk at create time instead, with volumes=[VolumeMount(...)].
Env vars and secrets
Two namespaces with identical methods — see Env vars & secrets. The difference is what comes back: anEnvVar has a readable value, a Secret has no value field at all.
set, delete and move each hand back the server’s confirmation message as a str.
Scope decides which machines a value reaches:
move changes the scope. It needs from_scope as well as to_scope, because the same name can exist in several scopes at once:
org="acme" to any of these to work in an organization instead of your personal scope.
Organizations
API keys
expires_in (seconds) for a key that expires. Every key belongs to exactly one organization: org names it, and omitting org uses your own. kind="member" (the default) acts as you within that organization; kind="org" is limited to the organization’s shared machines and requires an org admin to create.
Account
link_ssh_key takes the verbatim contents of a .pub file and lets you SSH to your machines with it. Pass device_id= to keep one key per device — re-linking from the same device replaces that device’s key instead of accumulating stale ones.
Errors
.message, .code (the canonical status name, e.g. "not_found") and .grpc_code (the numeric status code) for finer-grained handling:
Update notifications
The SDK prints a one-time note to stderr if a newer release is available:boxd.__version__.
Reference
CLI
Same API, accessed from the terminal. Useful for one-offs and shell scripting.
TypeScript SDK
The same surface for Node, Bun, and Deno.