Skip to main content
The boxd CLI, the SDKs, and the console are all clients of one thing: the public gRPC API at boxd.sh:9443. If your language doesn’t have a boxd SDK yet, generate one from the proto file and talk to it directly.

Installation

There is nothing to install from boxd’s side. Point a gRPC client at the endpoint, and generate stubs from the proto if your toolchain wants them.

Endpoint

  • Transport. TLS, with the same certificate the cluster’s HTTPS domains use. Plain HTTP/2 (h2c) is still accepted on the same port so clients released before TLS keep working. New integrations should connect over TLS. Auth is short-lived JWTs either way.
  • Reflection. gRPC server reflection (the v1 protocol) is enabled, so tools like grpcurl 1.9+ work without the proto file.
  • Service. boxd.api.v1.BoxdApi.

The proto

The full service definition lives on its own page: Proto. Copy it into a local api.proto and generate stubs with protoc, buf, grpc_tools, or @grpc/proto-loader. You often don’t need the file at all. Server reflection is on, so the schema can be inferred straight from the endpoint, and grpcurl boxd.sh:9443 describe boxd.api.v1.BoxdApi prints it on demand.

Authentication

Two steps: a long-lived API key is exchanged for a short-lived JWT, and the JWT rides as a bearer token on every gRPC call.

1. Create an API key

API keys are issued from the console. Sign in at app.boxd.sh, open the API keys page, and create one. The raw key is shown once, so copy it immediately. The format is bxd_ followed by 43 base62 characters. You can also create them with the CLI once you’re logged in:

2. Exchange for a JWT

Send the API key to the exchange endpoint on the console host, over HTTPS:
The JWT from an API-key exchange lives for 1 hour, so re-exchange before expiry. The endpoint is rate-limited per source IP.

3. Send it on every gRPC call

Standard gRPC metadata. Most generated clients expose this as a per-call interceptor or call option.

What an API-key session can do

An API key is fenced to one org. A member key (the default) covers the machine, snapshot, checkpoint, disk, port, proxy, domain, env/secret, file, and org and team surface, always for that one org. An org key is a userless service credential that reaches only the org’s shared machines, and env/secret and org operations reject it too. A handful of RPCs additionally require a JWT from an interactive login (the browser or CLI device flow) and answer PERMISSION_DENIED to API-key sessions: CreateApiKey, ListApiKeys, DeleteApiKey, CreateToken, RevokeToken, LinkSshKey, CreateNetwork, ListNetworks, GetBilling, GetOrgBilling, the checkout/portal calls, CreateOrg, and any invite or role change that would make someone an admin. GetOrgMembers lists pending invites to an API-key session without their tokens and links. The practical consequence is that an API key can never mint more keys, and never more standing than it has.

Commands

The full RPC surface is the service BoxdApi block in the proto: machines, files and directories, snapshots, checkpoints, disks, ports, proxies, domains, env vars and secrets, organizations and their members, teams, tokens, billing, and usage and cost. What a given RPC expects is documented on its request message, and grpcurl describe dumps any of it on demand. Egress control is SetVmEgressAllow plus the domains field of SetVarRequest. See Egress control. SSH on or off is SetSsh, or config.network.ssh on create. See Turn SSH off. Machine specs are the spec field of CreateVm and CreateVmFromSnapshot, plus UpdateMachineSpec. See Machine specs below.

Hello world

Easiest way to verify auth and reachability, with no proto file needed since server reflection is on.
Install: brew install grpcurl or github.com/fullstorydev/grpcurl.

Usage and cost

Three read-only RPCs answer “what did this cost”. They are for org owners and admins, and an API key fenced to the org can call them. A token minted inside a machine cannot. Inside a machine, boxd machine usage shows that machine’s own usage.
  • Money. Amounts are in micro-units (1e-6) of the org’s currency ("eur" or "usd"), so cost_micro / 1e6 is the amount.
  • Windows. Windows are unix seconds, [since, until), at most 366 days.
  • Costs are estimates. Usage is metered every second and settled every 15 minutes, and costs are priced at the org’s current rates. ListBillingActivity is what was charged: one usage_charge per 15 minutes for the whole org.
  • complete. The flag on a machine (and on the GetOrgUsage response) is true once all of its usage in the window is metered, so the figure will not grow. Metering lags by up to about 25 minutes. A machine that still exists is never complete in a window that runs to now. To record what a deleted machine cost, read it again until complete is true.
  • Shared names. A deleted machine’s name can be reused. If several deleted machines carried the name you pass, GetVmUsage answers INVALID_ARGUMENT and lists their ids.

Machine specs

A machine spec describes a machine in one value: what it boots from, its size, labels, networks, isolation, egress allowlist, env, the secrets it gets, the integrations and agent logins it needs, and shell commands to run on its first boot. The machine keeps it. See Machine specs for what each part does. MachineSpec holds version (must be 1), base (MachineSpecBase: team or snapshot), size ("<vcpu>x<memory GiB>", such as "2x8"), assign_to, labels, networks, isolated, egress, env, secrets (names), connections (integration name to "personal" or "shared:<tag>"), agents ("claude", "codex") and packages. Server reflection describes every message: grpcurl boxd.sh:9443 describe boxd.api.v1.MachineSpec.
  • The patch is a JSON string. patch is a JSON Merge Patch on the spec’s JSON form, the one the SDKs print (camelCase, so assignTo), sent as a string so that null and [] survive. A field it leaves out is untouched, a scalar replaces, maps merge key by key with null removing a key, and a list replaces the whole list. expected_spec is the spec you read, in the same form: if the machine’s spec is no longer that one, nothing is applied.
  • All or nothing. A change that can’t apply (base, isolated and assign_to after creation, packages on a fork, a size change on standby or hibernated) refuses the whole patch, and so does a size change for a running machine without reboot. A resize that fails after the rest of the patch applied still answers OK, with applied true, rebooted false and resize_error set.
  • Status codes. INVALID_ARGUMENT: a bad patch, or a spec the rules refuse, naming the field (spec.size: ...). PERMISSION_DENIED: a secret you may not list, assign_to your role or credential doesn’t allow (an org key, a token from inside a machine), or a change a machine may not make to itself from inside (its own egress or secrets). FAILED_PRECONDITION: a refused change, a reboot without reboot, or a machine without a spec. ABORTED: the spec changed while your update ran, and nothing was applied. Read it again and retry.
  • Egress. A new allowlist from a spec update is in force within about a second of the response.

Errors

Standard gRPC status codes. The most common ones you’ll hit: The error message in Status.message() is human-readable and safe to surface to users.

Streaming RPCs

Two RPCs stream:
  • Exec is bidirectional. The first message must contain vm_id and command. Subsequent client messages with stdin=true pipe stdin, or window_change=true with cols/rows resize a PTY. Server messages carry data, with is_stderr set for stderr chunks on non-PTY execs (PTY-mode execs merge stderr into stdout at the kernel). The final server message has exit_code set. Closing the client’s send half signals stdin EOF to the subprocess.
  • UploadFileStream is client-streaming and lifts the 4 MiB single-message gRPC cap of UploadFile. The first UploadFileChunk carries vm_id, path, and total_size, and subsequent chunks carry bytes only. The server reads exactly total_size bytes and returns the confirmed byte count.

Calling from a browser or Cloudflare Worker

Raw gRPC needs HTTP/2 trailers and a socket API that neither browsers nor Cloudflare Workers expose. Both are limited to fetch and WebSocket. boxd.sh:9443 also speaks grpc-web, a wire-compatible HTTP/1.1-friendly framing of the same protobuf messages, over the same port, for exactly this case. Everything above still applies: same endpoint, same Authorization: Bearer JWT (as a normal HTTP header this time, not gRPC metadata), same RPCs, same errors. The differences are only in how a message gets on the wire:
  • Content type. application/grpc-web+proto. Each message is one frame: a 1-byte flag, a 4-byte big-endian length, then the protobuf payload. The response ends with a trailer frame (flag bit 0x80 set) carrying grpc-status and grpc-message as key: value\r\n text. grpc-message is percent-encoded, so decode it before displaying it.
  • CORS. Enabled, so a browser can call directly with no server-side proxy.
  • Exec and UploadFileStream run half-duplex. The whole request (every message you’d have sent on a native gRPC stream) has to finish before the response starts streaming back, since fetch can’t read a response body while still writing the request body. This is already how exec() (send the command, read the result) and a file upload (send the bytes, read the confirmation) behave in every boxd SDK. The only thing that doesn’t work over grpc-web is an interactive PTY session, which needs a connection that’s live in both directions at once.
Unless you’re targeting a language with no grpc-web library, use the TypeScript SDK’s @boxd-sh/sdk/web entry point rather than framing grpc-web by hand. See TypeScript SDK. It implements the framing above once and exposes the same boxd.<namespace>.<verb>() surface as the Node SDK.

Minimal example (no library)