@boxd-sh/sdk 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, domains, 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 TypeScript.
Promise-only, ESM-only. Runs on Node 20+, Bun, and Deno.
Installation
VERSION.
Cloudflare Workers, browsers, and other non-Node runtimes
The default@boxd-sh/sdk entry point uses gRPC (@grpc/grpc-js), which needs Node’s http2/net modules and can’t load in a Cloudflare Worker or a browser. Import from @boxd-sh/sdk/web instead: same client, same namespaces, same options, same errors.
:9443 endpoint over grpc-web (plain fetch) — every call, including exec() and file uploads, goes over one wire protocol fetch can carry. It works anywhere fetch is available: Cloudflare Workers, browsers, Bun, Deno, Node 20+. Use the default @boxd-sh/sdk entry on a server unless you have a specific reason not to; it has no such restriction.
The one thing that doesn’t work on this entry is
streamExec() (interactive PTY sessions) — it needs a connection that’s live in both directions at once, which grpc-web can’t provide, and throws immediately telling you so. Ordinary exec() and file uploads are unaffected: both already send everything and then read the result, on either entry.Authentication
- an explicit
token - an explicit
apiKey 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.apiKeys.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 fails with AuthenticationError rather than retrying.
Inside a machine
Inside a boxd machine,new Boxd() authenticates automatically, with neither an API key nor configuration:
baseURL or credential still wins.
The client
baseURL selects the cluster.baseURL accepts an optional scheme that controls TLS:
Failed connections are retried up to
maxRetries times with exponential backoff. Timeouts are never retried, because the request may already have been applied, and authentication failures are never retried either.
Call close() when you’re done, or let the runtime do it:
await using needs Node 20+, Bun, or Deno.
Commands
Every call isboxd.<namespace>.<verb>(...), and nine namespaces hold the whole surface:
Quick start
The whole loop in one example, create through destroy:Machine is a record of fields rather than a handle, and every operation takes the machine id (or name) as its first argument.
Machines
Create, inspect, and control machines. The sub-namespacesboxd.machines.files, .ports, .proxies, .checkpoints, and .backups are covered further down in this section.
list() returns a plain array. Pass { org: "acme" } for one organization’s machines, or { allContexts: 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 rather than once it is usable. Call waitUntilReady before doing anything that depends on it running, and especially before forking it again. It polls for up to 90 seconds by default ({ timeout, pollInterval }, both in milliseconds).
Creating machines
create({}) boots the default image at the default size. fork(id, { name, shared, networks, isolated, config }) takes the same config, and anything you leave out is inherited from the source, including its networks.
isolated is a one-way flag on fork and on create({ fromSnapshot }). A fork or restore of an isolated machine is always isolated, and passing isolated: true isolates a copy of a machine that wasn’t. There is no way to remove isolation from a copy, and no setter afterwards.
A snapshot restores the machine that was captured, so
create({ fromSnapshot }) takes only name, org, config, and isolated. Passing image, env, cmd, restartPolicy, shared, or networks alongside it is a type error, and the two ways of creating a machine are separate shapes that the compiler keeps apart. Plain JavaScript, which has no compiler to catch it, gets the same refusal at runtime. (networks are replayed from the snapshot; set them afterwards with setNetworks.)Machine.
Networks
Two machines reach each other when they share at least one network, or when neither has any. Machines with no networks form the org’s default network, so naming a network opts a machine out of that pool. The same rule governs the<name>.boxd DNS name and exec / cp between machines, so what resolves is exactly what is reachable.
setNetworks replaces the whole set rather than merging, and [] is a real destination, the default network, rather than a no-op. Networks are labels you pick, with nothing to create first, and they never cross an organization. Changes take effect immediately.
isolated sandboxes a machine. It never joins the default network and is never a peer of another isolated machine, and boxd strips the in-VM boxd CLI, the metadata endpoint, and your integrations from it. An isolated machine with no networks therefore has no peers at all, and setNetworks is the only lever it has. It is fixed at creation, with no setter. Outbound internet and inbound SSH/HTTPS are unaffected. See Sandboxes.
The Machine object
org is the organization the machine belongs to and is billed to, and shared says whether your teammates can see it. A private machine can still be org-billed, so org set with shared: false is normal rather than a contradiction. See VM sharing.
source.id may not resolve if the machine or snapshot it points at was since deleted. A lookup that finds nothing is normal.
Every machine status the SDK knows is exported as MACHINE_STATUSES, with isKnownMachineStatus(s) to check one. An unrecognised status is passed through rather than thrown on, so a machine in a newly added state still prints.
Exec
One-shot exec collects the output:command is shell-quoted for you, while a string is passed through as a shell command line. timeout is in milliseconds and cancels the call, though the remote process may keep running. exec also takes tty, cols and rows. Under a PTY the terminal layer merges stderr into stdout, so stderr comes back empty and everything lands in stdout.
Interactive and PTY sessions use a stream handle, the one stateful object besides the client:
tty, stderr arrives separately on stream.stderr, which is useful when a tool’s progress goes to stderr and its answer to stdout. With tty, the terminal layer merges the two onto stdout, as terminals do.
For TUI apps, pass the initial geometry and forward resizes:
cols/rows for anything that draws to the terminal, since an unset size gives the PTY zero geometry and leaves the fallback to the program. 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 closeStdin: true to send EOF immediately, or call stream.end() yourself. It is rejected together with tty, where stdin must stay open.
Files
upload returns the number of bytes written, and large uploads are chunked for you. download returns a Uint8Array. 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.
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, because a named route is always pinned to a port. "auto" is only accepted for a machine’s default route, which is what setPort 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".
Backups
Off-machine disk backups for disaster recovery, on demand or on a schedule. See Disaster recovery. Restoring one rewrites the disk only, not memory or running state.intervalSecs or cron to setSchedule; a schedule can’t fire more often than every 15 minutes either way. trigger is independent of the schedule and runs one capture at a time per machine. delete is refused for a backup still being captured or one a newer "dedup" entry still points at; a "dedup" backup restores the same as a "ready" one.
Snapshots
Reusable, named captures. See Snapshots. Boot one withmachines.create({ fromSnapshot }).
Snapshot carries createdAt (the first capture), updatedAt (the most recent one), version, status, sizeBytes, vcpu, memoryBytes and useCount. 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, and read-only is chosen per attachment. A disk can be attached to one machine at a time. status is "creating", "ready" or "destroyed", and 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 config.volumes.
Env vars and secrets
Two namespaces with identical methods. See Env vars & secrets. The difference is what comes back: an env var has a readablevalue, a secret does not.
set, delete and move each hand back the server’s confirmation message as a string.
Scope decides which machines a value reaches:
move changes the scope. It needs from as well as to, 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.
Egress
Two controls, both enforced outside the machine. See Egress control. A per-machine allowlist of what it may reach. Each call replaces the whole set;[] clears it:
bxds_… placeholder; the real value is substituted into requests to those hosts on the way out. domains is the full desired set, and setting the secret again without it makes it a plain secret:
Domains
Bring your own domain instead ofname.boxd.sh. create binds the domain and starts verification immediately, so call it only once the DNS records are actually in place. The records to set (an A record plus a wildcard CNAME) and the verification flow are covered in Custom domains.
Organizations
Wildcard domain
A wildcard domain for the whole org: see Custom domains. Setting or clearing requires org admin, and any member can read it.API keys
expiresIn (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, while kind: "org" is limited to the organization’s shared machines and requires an org admin to create.
Account
linkSshKey takes the verbatim contents of a .pub file and lets you SSH to your machines with it. Pass deviceId to keep one key per device, so re-linking from the same device replaces that device’s key instead of accumulating stale ones. Like key management, linkSshKey requires an interactive-login credential rather than an API key.
Errors
BoxdError:
Errors returned by the server carry
grpcCode, the numeric status code, for finer-grained handling. Errors raised client-side, like an exec timeout or a rejected parameter, have no grpcCode: