# SDK reference

https://instabox.sh/docs/sdk.md

Every class, method and option of @insforge/instabox-sdk.

Version 0.5.0 of `@insforge/instabox-sdk`. For a walk through the common calls, start with [Integrate with the SDK](https://instabox.sh/docs/integrate.md).

### Install and authenticate
```
npm install @insforge/instabox-sdk
export INSTABOX_API_KEY=ibk_...
# optional; the default is https://api.instabox.sh
export INSTABOX_API_URL=https://api.instabox.sh
```
Every static call (`create`, `connect`, `list`, `kill`, the snapshot calls and the metrics and usage calls) also takes the key and URL as options, which win over the environment. A `Sandbox` object keeps its connection, so its own methods take no key.
```
import { Sandbox } from '@insforge/instabox-sdk'

const sbx = await Sandbox.create('agent-browser', {
  apiKey: process.env.INSTABOX_API_KEY,
  apiUrl: 'https://api.instabox.sh',
})
```
With no key from either, the call throws `InstaboxAuthError` with code `missing_api_key` (status 0) before any request. The key is sent as the `X-API-Key` header.

These connection options (`ConnectionOptions`) apply to every static call:

| Option | Type | Default |
|---|---|---|
| `apiKey` | `string` | env `INSTABOX_API_KEY` |
| `apiUrl` | `string` | env `INSTABOX_API_URL`, else `https://api.instabox.sh` |
| `requestTimeoutMs` | `number` | 60 000 per HTTP request; a command gets its own timeout plus 30 000 |
| `operationTimeoutMs` | `number` | 600 000: the wait for create, pause, resume and kill |
| `fetch` | `typeof fetch` | the global `fetch` (Node 18+, Bun, Deno) |
| `pollIntervalMs` | `number` | 250: first delay of operation polling, grows 1.5x to 1 s |
| `retryDelayMs` | `number` | 250: first delay between retries |

Scopes a key needs:

| Scope | Allows |
|---|---|
| `machines:read` | `Sandbox.list`, `getInfo`, `isRunning`, `getMetrics`, `getUsage`, `Sandbox.usage` |
| `machines:write` | `create`, `connect` (resume), `pause`, `resume`, `stop`, `setAutoSuspend`, `setMetadata`, `commands`, `files`, `sshCredentials` |
| `machines:delete` | `kill` |
| `images:read` | Resolve private snapshot names, `listSnapshots`, `getSnapshot` |
| `images:write` | `createSnapshot` |
| `images:delete` | `deleteSnapshot` |

`:write` and `:delete` include `:read`. Creating from a snapshot name also needs `images:read`. A call outside the key's scopes throws `InstaboxAuthError` (`403 missing_scope`).

### Sandbox.create
```
Sandbox.create(opts?: SandboxOpts): Promise<Sandbox>
Sandbox.create(template: string, opts?: SandboxOpts): Promise<Sandbox>
```
`template` is a template ID (`universal`), a private snapshot name or a snapshot ID (`sn_...`). Template IDs take precedence over snapshot names. The default is `agent-browser` (`DEFAULT_TEMPLATE`), or `universal` (`FALLBACK_TEMPLATE`) on a server without it. It resolves once the machine runs; a snapshot that is still being saved is waited for, up to `operationTimeoutMs`.

`SandboxOpts` is the connection options plus:

| Option | Type | Default |
|---|---|---|
| `name` | `string` | `sbx-<random>`; lowercase letters, digits and `-` |
| `metadata` | `Record<string, string>` | none; labels to filter `Sandbox.list` on |
| `envs` | `Record<string, string>` | none; added to every `commands.run` of this object (client side) |
| `archiveMs` | `number` | none; maps to API `archiveSeconds`. After this many ms from create, the server **stops** a still-running machine (`archiveAt`); disks stay and a later start restores it. Not a delete; omit for no archive deadline |
| `autoSuspendMs` | `number` | 30 minutes; pause after this much idle time. A paid plan takes 5-30 minutes, rounded up to 5, 10, 15, 20, 25 or 30 (below 5 or above 30 is refused), `0` never. The free plan always pauses after 30 idle minutes; a server from 2026-10-25 refuses another value (403) instead of ignoring it |
| `vcpus` | `number` | the template's |
| `memMiB` | `number` | the template's |
| `diskGiB` | `number` | the size's; the disk (`/data`), in GiB, sent as the API's `persistentDiskGiB` |
| `vault` | `boolean` | `false` disables only personal Vault delivery; org Secret selections are independent |

Times are sent as whole seconds. On api.instabox.sh a sandbox is 4 vCPUs and 8 GiB unless `vcpus` and `memMiB` name another size of your plan (Pro: 2/4 and 8/16; Team: 2/4 up to 32/64; `GET /v1/orgs` lists each workspace's as `allowedSpecs`). Any other size is refused with `400 spec_not_allowed`, and so is a `diskGiB` other than the disk the size comes with: leave it out. A paid sandbox's disk grows later with `PATCH /v1/machines/{id}` (see [Disk sizes](https://instabox.sh/docs/limits.md)), and its size changes while it is stopped with the same PATCH (`vcpus`, `memMiB`; see [Sizes](https://instabox.sh/docs/machines.md)). In a REST body the field is `persistentDiskGiB`; `diskGiB` there is `400 unknown_field`. The free plan creates from agent-browser or ci only (`403 template_not_allowed`); a sandbox from a snapshot is not limited.

**No kernel tracing, no nested KVM.** The guest kernel has the eBPF syscall and JIT (root only: unprivileged BPF is off, so use `sudo`), and socket, cgroup and XDP programs load. What it lacks is the tracing side: no kprobes, no ftrace or tracepoints, and no BTF (`/sys/kernel/btf/vmlinux` is absent), so `bpftrace`, BCC tools and `perf probe` do not work and there is nothing to attach a tracing program to. There is also no `/dev/kvm` in the guest (the kernel is a KVM guest, not a KVM host), so Firecracker, QEMU with KVM, Docker-in-VM runtimes that need it, and nested microVMs cannot run; QEMU in pure emulation (`-accel tcg`) works, slowly.

### Connect, list and kill
```
Sandbox.connect(sandboxId: string, opts?: SandboxConnectOpts)
  : Promise<Sandbox>
Sandbox.list(opts?: SandboxListOpts): Promise<SandboxInfo[]>
Sandbox.kill(sandboxId: string, opts?: ConnectionOptions)
  : Promise<boolean>
```
- `connect` attaches to an existing machine and resumes it if it is paused or stopped. `resume: false` attaches without waking it. `envs` works as in `create`.
- `list` returns every page of your machines. `metadata` keeps those with every given key and value.
- `kill` deletes a machine and its disks. It returns `false` if the machine did not exist.

### Snapshot calls
```
Sandbox.listSnapshots(opts?: ConnectionOptions): Promise<Snapshot[]>
Sandbox.getSnapshot(snapshotId: string, opts?: ConnectionOptions)
  : Promise<Snapshot>
Sandbox.deleteSnapshot(snapshotId: string, opts?: ConnectionOptions)
  : Promise<boolean>
```
`listSnapshots` returns your private snapshots. `deleteSnapshot` returns `false` if the snapshot was already gone. Save one with `sbx.createSnapshot`.

To copy a sandbox, save a snapshot and create from it:
```
const snap = await sbx.createSnapshot({ name: 'my-setup' })
const copy = await Sandbox.create(snap.id, { vault: false })
```
A copy uses its creation request's selections, not the source sandbox's grants. Omitting `secrets` does not enroll the copy in Secrets. `vault: false` disables only Vault delivery. To explicitly select Secrets, use the CLI or HTTP API.

### The Sandbox object
| Member | Returns | What it does |
|---|---|---|
| `sandboxId` | `string` | The machine ID, `m_...` |
| `getInfo()` | `Promise<SandboxInfo>` | Fresh state from the server |
| `isRunning()` | `Promise<boolean>` | True if `state` is `running` |
| `pause()` | `Promise<void>` | Saves memory and suspends it |
| `resume()` | `Promise<void>` | Resumes a paused machine, or starts a stopped one |
| `stop()` | `Promise<void>` | Shuts it down; processes end, `/data` is kept |
| `setAutoSuspend(idleMs: number)` | `Promise<void>` | Idle pause; a paid plan takes 5-30 minutes, rounded up to a 5-minute step (below 5 or above 30 is refused), and `0` turns it off, the free plan keeps 30 minutes and refuses another value. Kept across pause and resume |
| `setMetadata(metadata: Record<string, string>)` | `Promise<void>` | Replaces the labels; `{}` clears them |
| `createSnapshot(opts: CreateSnapshotOpts)` | `Promise<Snapshot>` | Saves its disks as a private snapshot |
| `getMetrics(opts?: SandboxMetricsOpts)` | `Promise<SandboxMetrics[]>` | CPU and memory samples, oldest first |
| `getUsage(opts?: SandboxUsageOpts)` | `Promise<SandboxUsage>` | What it used over a window |
| `sshCredentials(publicKey: string, opts?: { ttlSeconds?: number })` | `Promise<SshCredentials>` | A short-lived SSH certificate for `publicKey` |
| `kill()` | `Promise<boolean>` | Deletes it and its disks; `false` if already gone |
| `commands` | `Commands` | Run shell commands |
| `files` | `Files` | Read and write files |
| `desktop` | `Desktop` | See and control the desktop |

Lifecycle calls (`create`, `connect`, `pause`, `resume`, `stop`, `kill`) wait for the server's operation to finish, up to `operationTimeoutMs`.

`getInfo()` returns a `SandboxInfo`: `sandboxId`, `templateId`, `snapshotId?`, `name`, `metadata`, `vault`, `startedAt`, `archiveAt?`, `autoSuspendMs?`, `state` and `machine` (the raw API object). `state` is one of:
```
creating  starting  running  stopping  stopped
suspending  suspended  resuming  deleting  deleted  error
```
A paused machine is `suspended`. The `vault` field describes personal Vault delivery. Vault inspection and push endpoints remain available independently of `GET /v1/machines/{id}/secrets`, which returns org Secret selections and delivery status. Use the [Secret CLI or HTTP API](https://instabox.sh/docs/secrets.md) to manage those selections.

`CreateSnapshotOpts`: `name` (required), `description`, and `rootOnly` to save only the root disk. From a running machine the snapshot comes back with `status: 'pending'` and turns `ready` once saved.

`SshCredentials`: `host`, `port`, `username` (the machine ID), `certificate` (save it as `<key>-cert.pub`), `knownHostsLine`, `expiresAt` and `connectionId`. The machine must be running. `ttlSeconds` is 1 to 86 400; the default is 1 hour. See [SSH](https://instabox.sh/docs/ssh.md).

### sbx.commands
```
run(cmd: string, opts?: CommandOptions & { background?: false })
  : Promise<CommandResult>
run(cmd: string, opts: CommandOptions & { background: true })
  : Promise<CommandHandle>
```
| Option | Type | Default |
|---|---|---|
| `cwd` | `string` | the server's: HOME, `/data/home` |
| `envs` | `Record<string, string>` | none; merged over the `envs` given to `Sandbox.create` |
| `timeoutMs` | `number` | the server's; sent as whole seconds, at most 600 000 |
| `stdin` | `string` | none; at most 1 MiB of UTF-8 (`MAX_STDIN_BYTES`) |
| `onStdout` | `(data: string) => unknown` | called one time, with the whole stdout, when the command exits |
| `onStderr` | `(data: string) => unknown` | the same, for stderr |
| `background` | `boolean` | `false`; `true` starts it detached and returns at once |

The result (`CommandResult`) is `{ stdout, stderr, exitCode, timedOut, truncated, error? }`. Each output stream is cut at 1 MiB, and `truncated` says so. The SDK reads the whole result; for the output as the command writes it, call `POST /v1/machines/{id}/exec` with `Accept: text/event-stream` or run `instabox exec --stream`.

A non-zero exit throws `CommandExitError`, which carries the same fields. A command that runs past `timeoutMs` throws it with `timedOut: true` and exit code 124.
```
import { CommandExitError } from '@insforge/instabox-sdk'

try {
  await sbx.commands.run('exit 3')
} catch (e) {
  if (e instanceof CommandExitError) console.log(e.exitCode, e.stderr)
}
```
With `background: true`, `run` returns a `CommandHandle`: `pid` (the shell, which leads the process group) and `kill()`, which sends SIGTERM to everything it started and returns `false` if all had exited.
```
const server = await sbx.commands.run('node server.js', {
  background: true,
})
await server.kill()
```
`exec(cmd, opts?)` takes `cwd`, `envs`, `timeoutMs` and `stdin` and returns the `CommandResult` without throwing on a non-zero exit. It is marked internal, so prefer `run`. Commands are never retried.

### sbx.files
```
read(path: string, opts?: { format?: 'text' }): Promise<string>
read(path: string, opts: { format: 'bytes' }): Promise<Uint8Array>
write(path: string, data: string | Uint8Array): Promise<WriteInfo>
list(path: string): Promise<EntryInfo[]>
remove(path: string): Promise<void>
exists(path: string): Promise<boolean>
```
- `read` returns UTF-8 text, or bytes with `{ format: 'bytes' }`.
- `write` creates parent directories and replaces the file atomically. It returns `{ path }`. Writing to a directory throws `is_a_directory`.
- `list` returns `{ name, path, type }` entries, `type` being `'file'` or `'dir'`. It is not recursive.
- `remove` deletes a file or a directory, recursively. A missing path is fine.

Files travel through the command channel as base64, so they need the same scope as commands. Each read or write is at most 10 MiB (`MAX_FILE_BYTES`). Relative paths start at HOME, `/data/home`, on the persistent disk.

### sbx.desktop
```
start(opts?: { width?: number; height?: number })
  : Promise<DesktopStatus>
status(): Promise<DesktopStatus>
screenshot(): Promise<Uint8Array>
click(x: number, y: number,
  opts?: { button?: MouseButton; double?: boolean }): Promise<void>
move(x: number, y: number): Promise<void>
drag(from: [number, number], to: [number, number]): Promise<void>
scroll(opts: { direction: ScrollDirection; amount?: number })
  : Promise<void>
type(text: string, opts?: { delayMs?: number }): Promise<void>
press(keys: string | string[]): Promise<void>
open(url: string): Promise<void>
cursorPosition(): Promise<{ x: number; y: number }>
streamUrl(opts?: { viewOnly?: boolean; ttlSeconds?: number })
  : Promise<DesktopStream>
revokeStreamUrls(): Promise<void>
```
- `start` resolves once the desktop runs and leaves a running one as it is. On a template without the desktop it installs it first, about half a minute, once. Screen: 800 to 2560 by 600 to 1600, default 1280 x 800.
- `status` returns `DesktopStatus`: `installed`, `running`, and `width` and `height` while running.
- `screenshot` returns a PNG of the whole screen.
- `click` uses the left button unless `button` is `'right'` or `'middle'`; `double` clicks twice. Coordinates are pixels from the top left.
- `drag` holds the left button. `scroll` turns the wheel at the pointer, `amount` notches (default 3), `direction` `'up'`, `'down'`, `'left'` or `'right'`.
- `type` types the text as is, quotes, newlines and Unicode included, into the focused window.
- `press` takes a key (`'Enter'`, `'a'`, `'F5'`) or a chord (`['ctrl', 'l']`).
- `open` opens an http or https URL in the desktop's browser.

Input and screenshots run commands in the machine, so a failing one throws `CommandExitError`. The desktop runs on X display `:1` (`DESKTOP_DISPLAY`).

**The stream link.** `streamUrl` returns `{ url, expiresAt }`: a page that shows the desktop full-screen, to open or put in an `<iframe>`. Anyone with the link can connect, no instabox account needed, until `expiresAt` (`ttlSeconds` 60 to 86 400, default 3 600). It is view-only unless `viewOnly: false`. Expiry stops new connections only; `revokeStreamUrls()` ends every link of the machine and disconnects open viewers within a few seconds. See [Desktop](https://instabox.sh/docs/desktop.md).

### Metrics and usage
```
sbx.getMetrics(opts?: SandboxMetricsOpts): Promise<SandboxMetrics[]>
Sandbox.getMetrics(sandboxId: string,
  opts?: SandboxMetricsOpts & ConnectionOptions)
  : Promise<SandboxMetrics[]>
sbx.getUsage(opts?: SandboxUsageOpts): Promise<SandboxUsage>
Sandbox.getUsage(sandboxId: string,
  opts?: SandboxUsageOpts & ConnectionOptions)
  : Promise<SandboxUsage>
Sandbox.usage(opts?: ConnectionOptions): Promise<Usage>
```
All of them need `machines:read`; `Sandbox.usage` also works with `images:read`.
```
// the last 15 minutes of CPU and memory
const samples = await sbx.getMetrics({
  since: new Date(Date.now() - 15 * 60_000),
})
// what it used since the start of October (UTC)
const usage = await Sandbox.getUsage(sbx.sandboxId, {
  from: new Date('2026-10-01T00:00:00Z'),
})
console.log(usage.runningSeconds, usage.vcpuSeconds)
```

**Metrics.** `getMetrics` returns one sample per 30 s, oldest first, measured on the host. `since` (a `Date`) is the start of the window: by default 5 minutes ago, at most 24 h back. There are samples only for the times in the window the machine ran: a machine paused or stopped now still returns the samples from when it ran in the window, and one that did not run in the window gets an empty list. The last sample of a running machine is its host's latest report (at most about 30 s old, CPU and memory only), so a new machine shows its first sample within about 30 s. Every account can call this for its own sandboxes (not an admin preview). The CLI prints the same: `instabox metrics <id-or-name> [--json]`. `instabox get --json` also exposes `vcpus` and `memMiB` at the top level (and under `spec`). Each `SandboxMetrics`:

| Field | Type | What |
|---|---|---|
| `timestamp` | `Date` | When the sample was taken |
| `cpuUsedPct` | `number` | CPU time over the machine's vCPUs, 0 to 100 |
| `cpuCount` | `number` | vCPUs |
| `memUsed` | `number` | Guest memory touched, in bytes |
| `memTotal` | `number` | Memory size, in bytes |
| `diskTotal` | `number` | Disk (`/data`) size, in bytes |

On the latest sample of a running machine, `diskUsed` is the bytes used on `/data`, read inside the machine (the last read, refreshed in the background at most once a minute and at most 10 minutes old). The REST route `GET /v1/machines/{id}/disk` and `instabox disk <box>` show `/data` and `/` (used, free, size, inodes) with a level (`ok` below 80%, `high` from 80%, `critical` from 90%, `full` at 100%) and a warning that says how to free or add space.

**Usage of a machine.** `getUsage` returns what a machine used between `from` and `to` (both `Date`s), for billing your own customers. The default range is the current UTC month: `from` is the start of the month, `to` is now, and `to` may be at most 366 days after `from`. A `SandboxUsage`:

| Field | Type | What |
|---|---|---|
| `from`, `to` | `Date` | The window |
| `runningSeconds` | `number` | Seconds running, not paused or stopped |
| `vcpuSeconds` | `number` | vCPUs times running seconds |
| `memGiBSeconds` | `number` | Memory GiB times running seconds |
| `diskGiBSeconds` | `number` | Disk (`/data`) size times the seconds it existed in the window, paused or stopped included |

A deleted machine's usage stays readable by its account, by ID. Running time is recorded from the deploy of 2026-10-14; earlier time is not counted.

**Usage of the account.** `Sandbox.usage()` returns your counts, stored bytes and limits (`GET /v1/usage`). Bytes are measured about hourly.
```
interface Usage {
  computedAt: string | null // when bytes were measured
  logicalBytes: number
  storedBytes: number
  exclusiveBytes: number // deprecated: always 0
  machines: number
  snapshots: number
  limits: UsageLimits // maxMachines, maxSnapshots, maxStoredGiB,
                      // machineSpec?, liveSnapshots?, vault?
  storage?: UsageStorage // storedBytes by machine
}
```
`storage` has `machines` (each with `id`, `name`, `state`, `sizeBytes`, `diskBytes`, `snapshots` and `snapshotBytes`), `deletedMachineSnapshots` and `deletedMachineSnapshotBytes`. `computedAt` is null and `storage` absent before your first measurement.

### Errors
| Class | When |
|---|---|
| `InstaboxError` | Base class of every error. Fields: `status`, `code`, `message`, `requestId`, `retryable`, `operationId` |
| `InstaboxAuthError` | 401 or 403: a bad or revoked key, or a missing scope. No key at all: `missing_api_key`, status 0 |
| `InstaboxQuotaError` | 429, such as `quota_exceeded` |
| `InstaboxTimeoutError` | A request or an operation did not finish in time |
| `CommandExitError` | A command exited non-zero. Also has `stdout`, `stderr`, `exitCode`, `timedOut`, `truncated`, `error` |

`status` is 0 for errors that did not come from an HTTP response. A failed operation (for example `no_capacity` while creating) throws `InstaboxError` with the operation's `code` and `operationId`.

| Code | Meaning |
|---|---|
| `missing_api_key` | No key in the options or the environment |
| `missing_scope` | The key lacks a scope the call needs (403; older servers: `insufficient_scope`) |
| `missing_key`, `malformed_key`, `invalid_key`, `expired_key`, `revoked_key` | The key was not sent, is not a key, does not exist, expired or was revoked (401) |
| `not_found` | No such machine or snapshot |
| `operation_conflict` | Another operation runs on the machine (409); `operationId` names it |
| `quota_exceeded` | An account limit was reached (429) |
| `spec_not_allowed` | A size your plan does not offer, a disk size at create, or growing a free sandbox's disks (400) |
| `template_not_allowed` | A template your plan does not offer: the free plan creates from agent-browser or ci only (403) |
| `is_a_directory` | `files.write` to a directory |
| `request_timeout`, `operation_timeout` | A request or an operation took too long |
| `network_error` | The request did not reach the API |

Reads, and changes (which carry an `Idempotency-Key`), are retried up to 3 times with backoff on 502, 503, 504 and network errors.

### Types
| Type | What |
|---|---|
| `ConnectionOptions` | `apiKey`, `apiUrl` and the timeouts every static call takes |
| `SandboxOpts`, `SandboxConnectOpts`, `SandboxListOpts` | Options of `create`, `connect`, `list` |
| `SandboxInfo` | What `getInfo` and `list` return |
| `Machine`, `MachineSpec`, `MachineState` | The raw machine from the API, its size, its state |
| `CreateSnapshotOpts`, `Snapshot` | Snapshot options, a saved snapshot |
| `SandboxMetrics`, `SandboxMetricsOpts` | `getMetrics` samples and options |
| `SandboxUsage`, `SandboxUsageOpts` | `getUsage` result and options |
| `Usage`, `UsageLimits`, `UsageStorage`, `UsageMachineStorage` | What `Sandbox.usage` returns |
| `SshCredentials` | What `sshCredentials` returns |
| `CommandOptions`, `CommandResult`, `CommandHandle` | `commands.run` options and results |
| `EntryInfo`, `WriteInfo` | `files.list` entries, `files.write` result |
| `DesktopStatus`, `DesktopStream` | `desktop.status` and `desktop.streamUrl` results |
| `MouseButton`, `ScrollDirection` | `'left'`, `'right'`, `'middle'`; `'up'`, `'down'`, `'left'`, `'right'` |
| `Template`, `TemplateRef`, `Operation`, `ApiError` | API objects |

Constants: `DEFAULT_API_URL`, `DEFAULT_TEMPLATE`, `FALLBACK_TEMPLATE`, `MAX_FILE_BYTES` (10 MiB), `MAX_STDIN_BYTES` (1 MiB), `DESKTOP_DISPLAY`.
