Skip to main content
The @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, 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.

Install

Quick start

The client is the only stateful object. Everything else is a namespace of flat methods that return plain data — a Machine is a record of fields, not a handle, and every operation takes the machine id (or name) as its first argument. There are eight namespaces:

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 — 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.

Authentication

Credentials are resolved in this order, first match wins:
  1. an explicit token
  2. an explicit apiKey
  3. BOXD_TOKEN
  4. BOXD_API_KEY
  5. automatic, inside a boxd machine (below)
If none of those produce a credential, the first request throws 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 — no API key, no configuration:
It also targets the cluster the surrounding machine belongs to, so the same code runs unchanged wherever it is deployed. An explicit baseURL or credential still wins.
Inside a shared machine the SDK can manage the organization’s shared machines, but it cannot read env vars or secrets, and cannot reach machines that are private to another member. Pass an API key for those.

Machines

list() returns a plain array. Pass { org: "acme" } for one organization’s machines, or { allContexts: true } for everything you can reach. State:
See Suspend & resume for the difference between stop/start (cold) and pause/resume (warm). Everything else:
rename reboots the machine. It is its own call rather than part of an update() for exactly that reason.
create and fork return once the machine is scheduled, not once it is usable. Call waitUntilReady before doing anything that depends on it running — especially before forking it again. It polls for up to 90 seconds by default ({ timeout, pollInterval }, both in milliseconds).

Creating

Every field is optional — create({}) boots the default image at the default size. fork(id, { name, shared, config }) takes the same config; anything you leave out is inherited from the source.
A snapshot restores the machine that was captured, so create({ fromSnapshot }) takes only name, org and config. Passing image, env, cmd, restartPolicy or shared alongside it is a type error — the two ways of creating a machine are separate shapes, and mixing them is rejected before the code runs. Plain JavaScript, which has no compiler to catch it, gets the same refusal at runtime.
Renaming reboots the machine, so it is its own call. Everything else is readable straight off 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. 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:
An array command is shell-quoted for you; a string is passed through as a shell command line. timeout is in milliseconds and cancels the call — 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:
Without tty, stderr arrives separately on stream.stderr — 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:
Unset 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 closeStdin: true to send EOF immediately — or call stream.end() yourself. It is rejected together with tty, where stdin must stay open.

Logs

follow: true keeps the stream open for new output.

Files

upload returns the number of bytes written; 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.
Re-exposing the same machine port keeps its public port and just updates the protocol; "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 — 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".

Snapshots

Reusable, named captures — see Snapshots. Boot one with machines.create({ fromSnapshot }).
Saving under an existing name adds a version. A 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; 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 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 readable value, 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:
It is a move between two places, not a field update, so calling it twice fails the second time. Env vars and secrets share one name space per scope, so an existing env var can block a secret moving into that scope, and vice versa. Pass { org: "acme" } to any of these to work in an organization instead of your personal scope.

Organizations

API keys

Pass 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; kind: "org" is limited to the organization’s shared machines and requires an org admin to create.
create is the only time the raw key is returned. list shows the prefix only. Deleting a key takes effect immediately.

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 — re-linking from the same device replaces that device’s key instead of accumulating stale ones.

Errors

Everything thrown extends BoxdError: Every error carries grpcCode, the numeric status code, for finer-grained handling:

Update notifications

The SDK prints a one-time console.warn on stderr when a newer release is available:
It fires at most once per process and never causes a request to fail. The installed version is also exported:

Reference

CLI

Same API, accessed from the terminal. Useful for one-offs and shell scripting.

Python SDK

The same surface, sync and async, for Python codebases.