TypeScript SDK
Install @cubicle/sdk (ESM only, Node.js 22 or later), configure it with CB_SDK_API_URL and CB_SDK_API_KEY or client options, address Boxes by ID, ORG/NAME or name, read BoxInfo records with boxes.get and boxes.list, act through Box and BoxRef handles from create and ref, and create, run commands in and delete Boxes with a service account API key.
@cubicle/sdk is the TypeScript client for the Cubicle HTTP API. It creates, starts, stops,
hibernates and deletes Boxes, runs commands in them, takes and restores snapshots, and follows
operations, all with a service account API key. Calls that change a Box wait until the change
has taken effect, so when await returns, the Box is ready, stopped, hibernated or gone.
Install
npm install @cubicle/sdkRequirements
- Node.js 22 or later. Other runtimes are untested.
- ES modules. The package is ESM only, so import it from an ES module, such as a
.mtsfile or a project with"type": "module". - No runtime dependencies. The SDK uses
fetch,AbortSignal,crypto.randomUUID()and other web-standard APIs, and no Node built-ins. - No local state. Every request carries the API key. Nothing is written to disk, and there is no session to renew.
- A service account API key (
cbk_…), withwritescope for any call that changes a Box or runs a command. See Sign in and API keys and Run CI jobs in Boxes. - A grant on each Box you run commands in, and a grant or the
owneroradminrole on each Box you start, stop, hibernate, delete, snapshot or restore. A service account gets a grant on every Box it creates; see Box grants.
Keep API keys out of browsers
An API key acts with all of its service account's access. Use the SDK in server-side code, CI jobs and agents. Never ship a key in code that runs in a browser.
Configure the client
new Cubicle() takes its API URL and API key from the first place that sets each one:
- The
apiUrlandapiKeyoptions passed tonew Cubicle({ … }). - The
CB_SDK_API_URLandCB_SDK_API_KEYenvironment variables, where the runtime hasprocess.env.
If either is still missing, the constructor throws a CubicleError with code missing_api_url
or missing_api_key. The constructor sends no request, so a wrong key shows up on the first
call, as a CubicleApiError with code unauthenticated.
export CB_SDK_API_URL=https://api.cubicle.sh
export CB_SDK_API_KEY=cbk_…import { Cubicle } from "@cubicle/sdk"
const cubicle = new Cubicle() // reads CB_SDK_API_URL and CB_SDK_API_KEYPass apiKey when the key comes from somewhere else, such as a secret manager. Options win over
the environment.
Client options
| Option | Env var | Default | Meaning |
|---|---|---|---|
apiUrl | CB_SDK_API_URL | None: required | The API URL, https://api.cubicle.sh. It must use http or https. |
apiKey | CB_SDK_API_KEY | None: required | A service account API key, cbk_…. |
org | — | The API key's organization | Organization name or ID for boxes.create, boxes.list, listBases and listSnapshots. It also narrows bare Box names. |
fetch | — | The global fetch | A replacement fetch, for proxies or tests. |
requestTimeoutMs | — | 30000 (30 s) | Timeout for each attempt of a request, from 1 to 2^31 − 1 ms. |
maxRetries | — | 3 | Retries for each request after a rate limit, a 502, 503 or 504, a connection failure or a request timeout, from 0 to 100. Waits do not use it. |
pollIntervalMs | — | 1000 (1 s) | Delay between one wait's polls, from 1 to 2^31 − 1 ms. |
maxPollsPerSecond | — | 5 | The most polls a second across all of the client's waits, from 0.001 to 1000. |
The constructor throws a CubicleError with code invalid_option when a value is out of range.
A wait's timeoutMs that is negative or not a number rejects with invalid_option too, when the
wait begins, after the request was sent; see Change the timeout.
Values the API checks, such as a Box name, a size or an exec timeoutMs under 100 ms, reject
with a CubicleApiError with code invalid_request.
Retries are described in Errors and retries and polling in
Waiting. Every option is listed in
CubicleOptions.
Use one client per process and share it, because its waits share one poll budget. Separate clients share nothing in the SDK, but every API key of one service account still spends the same rate limit.
The default organization
An API key belongs to one organization, so you rarely need to name one. Calls that need an
organization (boxes.create, listBases and listSnapshots) take it from the first of:
- The call's own
org:params.orgforboxes.create, or theorgoption oflistBasesandlistSnapshots. - The client's
orgoption. - The API key's organization. The client reads it from
GET /v1/meon first use and remembers it. A failed lookup is tried again on the next call.
If none applies, which happens only with a credential that is not an API key and does not belong
to exactly one organization, the call throws a CubicleError with code org_required.
boxes.list() with no org, on a client with no org option, lists every organization the
caller can see; for an API key, that is its own organization. Bare Box names are narrowed to the
client's org, or else to the API key's organization.
What the client offers
A Cubicle client has these members:
| Member | What it does | Reference |
|---|---|---|
cubicle.boxes.* | Create, read, list, start, stop, hibernate and delete Boxes; run commands; snapshot and restore; wait for a state; list operations. | Boxes |
cubicle.operations.* | get(id) and wait(id) for exec, snapshot and restore operations. | Operations |
cubicle.me() | The caller's identity, organizations and API key. | Me |
cubicle.listBases() | Base images, and whether a healthy Node has each one. Free capacity is not considered. | CreationBase |
cubicle.listSnapshots() | Completed snapshots you may create Boxes from. | CreationSnapshot |
Boxes and handles
Each cubicle.boxes method that acts on one Box takes a Box reference as its first argument: the
Box's ID, ORG/NAME, or a bare NAME, narrowed as in
The default organization. The SDK URL-encodes the reference, so
acme/job-1 is sent as acme%2Fjob-1. How each form resolves is in
Box references. A deleted Box's name can be reused at once, so
store box.id when you need to reach the same Box later.
| Call | Returns | Network request | Use it to |
|---|---|---|---|
cubicle.boxes.get(ref) | BoxInfo record | GET /v1/boxes/{box} | Read the Box's current state. It never changes the Box. |
cubicle.boxes.list() | BoxInfo records | GET /v1/orgs/{org}/boxes, or GET /v1/boxes without an organization | List Boxes. Boxes being deleted stay listed, with deletion: "pending", until they are gone. |
cubicle.boxes.ref("acme/job-1") | BoxRef handle | None | Act on a Box by reference. A missing Box is reported by the handle's first call. |
cubicle.boxes.ref(info) | Box handle | None | Turn a record from get or list into a handle with id, name and org. |
cubicle.boxes.create(…) | Box handle | POST /v1/orgs/{org}/boxes, then polls | Create a Box and get a handle for it. |
- A record,
BoxInfo, is the Box as the API reported it at that moment. It does not update itself. Its fields are explained in The Box object. - A handle,
BoxReforBox, holds a reference, not a record. Its methods areinfo(),start(),stop(),hibernate(),delete(),exec(),waitUntil(),waitUntilReady(),snapshot(),listSnapshots(),restore()andlistOperations(): thecubicle.boxesmethods bound to one Box, sobox.stop()andcubicle.boxes.stop(box.id)do the same thing.info()callsget()for its Box. - A
Boxalso hasid,nameandorg, and addresses its Box by ID. ABoxRefhas onlyref, the reference it sends.
const info = await cubicle.boxes.get("acme/job-1")
console.log(info.state, info.ssh.ready) // => "running" true
const job = cubicle.boxes.ref("acme/job-1") // no request yet
await job.stop()
const handle = cubicle.boxes.ref(info) // a Box, addressed by ID
console.log(handle.id, handle.name) // => "8d0c6a8e-…" "job-1"What each method does, waits for and rejects with is in the
Boxes reference. The tasks, with examples, are in
Run commands, Snapshot and restore a Box and
Stop, hibernate and wake a Box.
Example: create, run a command, delete
This program creates a Box, runs a command in it, and deletes the Box even when the command fails.
import { Cubicle } from "@cubicle/sdk"
const cubicle = new Cubicle()
// Waits until the Box is ready for commands, up to 5 minutes.
const box = await cubicle.boxes.create({ name: "job-1" })
try {
const result = await box.exec(["uname", "-a"])
console.log(result.exitCode, result.stdout)
// => 0 "Linux box-job-1 …"
} finally {
// Waits until the Box's Node has removed it.
await box.delete()
}It uses top-level await, so run it as an ES module: for example, save it as job.mts and run
npx tsx job.mts.
Box names are unique in an organization, so running it again while job-1 still exists rejects
with conflict. Omit name to get a generated one.
Not in the SDK yet
- Streaming exec output, stdin and terminals.
execreturns the output when the command ends. - File upload and download. See Move files into and out of a Box.
- Network connections to a Box. The SDK cannot open one, and there are no port forwards, proxies
or public URLs. To reach a service in a Box from code, run the client inside the Box with
exec, for example againstlocalhost. - SSH. Service accounts cannot register SSH keys; use
cubicle sshwith your own sign-in. - Fork, resize, rename and reboot.
Everything Cubicle does not offer yet is in Limits and defaults.

