# The TypeScript SDK

`@sandboxio/sdk` is the Sandbox Router API ([The API](/docs/api)) as typed TypeScript methods: sandboxes and their execs, files, logs, snapshots and access tokens; MCP servers; providers; connectors and their workspace accounts; your users' connections and sign-ins; and your workspace's settings, audit log and billing. It has no runtime dependencies and runs on Node 22 or later, Deno, Bun and Cloudflare Workers.

## Install the SDK

```sh
npm install @sandboxio/sdk
```

## Connect it to your workspace

The SDK sends an API key from your workspace. Create one in the [console](https://console.sandboxrouter.io/) under **Settings**, as [The API](/docs/api) describes, and set it as `SANDBOXROUTER_API_KEY`:

```sh
export SANDBOXROUTER_API_KEY=<api-key>
```

A client reads `SANDBOXROUTER_API_KEY` when you don't pass `apiKey`:

```ts
import { SandboxRouter } from "@sandboxio/sdk";

const sandboxrouter = new SandboxRouter();
```

Every request acts in the key's workspace.

## Run a sandbox

```ts
const sandbox = await sandboxrouter.sandboxes.create({ labels: { name: "build-agent" }, size: "m", egress: ["registry.npmjs.org"] });

const result = await sandboxrouter.sandboxes.exec(sandbox.id, { command: ["node", "--version"] });
console.log(result.exitCode, result.stdout);

await sandboxrouter.sandboxes.files.write(sandbox.id, "/workspace/hello.txt", "hello\n");
const text = await sandboxrouter.sandboxes.files.readText(sandbox.id, "/workspace/hello.txt");

await sandboxrouter.sandboxes.stop(sandbox.id);
await sandboxrouter.sandboxes.delete(sandbox.id);
```

`create`, `start`, `stop` and `restart` each answer the sandbox once it settled, holding as long as that takes. Every method that names a sandbox takes its ID. An exec's command is an array, which runs as it is, or a string, which runs through `/bin/sh -c`. A command that exits nonzero isn't an error: read its `exitCode`. A stopped sandbox stays stopped until a start: an exec or a file call answers `not_running`.

### Long-running commands

Pass `detach: true` to get a handle back at once, and wait for the command or cancel it later:

```ts
const job = await sandboxrouter.sandboxes.exec(sandbox.id, { command: ["make", "release"], detach: true, timeoutSeconds: 86_400 });
const done = await job.wait();
```

A detached command's output arrives when it ends.

### Files, logs, metrics, ports and snapshots

```ts
await sandboxrouter.sandboxes.files.write(sandbox.id, "/workspace/app.js", bytes); // a file, whole
await sandboxrouter.sandboxes.files.delete(sandbox.id, "/workspace/app.js");

for await (const entry of sandboxrouter.sandboxes.followLogs(sandbox.id)) console.log(entry.kind, entry.message);

// its provider's own CPU, memory and disk samples, oldest first, null where it doesn't measure one
const { samples } = await sandboxrouter.sandboxes.metrics(sandbox.id, { from: Date.now() - 3_600_000 });

const { url } = await sandboxrouter.sandboxes.port(sandbox.id, 3000); // its provider's own URL, while it runs

const snapshot = await sandboxrouter.sandboxes.snapshots.create(sandbox.id); // its provider's own
await sandboxrouter.sandboxes.snapshots.delete(sandbox.id, snapshot.id);
```

## MCP servers

```ts
const server = await sandboxrouter.mcpServers.create({
  name: "search",
  sandbox: { image: "registry.caas.sh/<tenant-id>/search-mcp@sha256:…", egress: ["api.search.example"] },
  transport: { http: { port: 8080 } },
});
```

[MCP servers](/docs/mcp) explains what each field does.

## Your users

`sandboxrouter.users` signs your users in and makes their connect links, one user at a time (see [Connect each user's account](/docs/connectors#connect-each-users-account)). Sign a user in to a connector they sign in to with a code, such as Codex (ChatGPT), before a sandbox of theirs needs it (see [Sign users in with a code](/docs/connectors#sign-users-in-with-a-code)); `waitForSignIn` waits until they have, calling `onCode` with each code to show them:

```ts
await sandboxrouter.users.signIn("user_123", "codex");
await sandboxrouter.users.waitForSignIn("user_123", "codex", { onCode: (signIn) => showToUser(signIn.code!, signIn.url!) });
const sandbox = await sandboxrouter.sandboxes.create({ user: "user_123", connectors: ["codex"] });
```

`waitForSignIn` reads the user's connection again after each answer's `pollAfterMs`, never on a timer of its own, and calls `onCode` with each code it hasn't shown yet. It resolves with the connection once the user is signed in, and rejects with a `SandboxRouterSignInError` (its `signInStatus`, `expired`, `failed` or `none`, and `reason`) when the code expires or the sign-in fails, or when there's no sign-in to wait for. Pass a `signal` to stop waiting.

```ts
const { signIn } = await sandboxrouter.users.signIn("user_123", "codex"); // the code under way, a new one, or a sign-in again for a user who's signed in
const { connectUrl } = await sandboxrouter.users.link("user_123", { connector: "linear", returnUrl: "https://app.acme.com/settings" });
const connection = await sandboxrouter.users.connection("user_123", "linear");
await sandboxrouter.users.disconnect("user_123", "linear");
```

`sandboxrouter.users.logins` manages a user's [logins](/docs/logins), their sign-ins to websites, which their sandboxes' browsers sign in with; `sandboxrouter.connectors.account.logins` has the same methods for your workspace's logins, but `link`, its first argument the Logins connector's ID:

```ts
const login = await sandboxrouter.users.logins.create("user_123", { site: "github.com", username: "ana@acme.com", methods: ["passkey", "typed"], password });
const { connectUrl } = await sandboxrouter.users.logins.link("user_123", { site: "github.com" }, { returnUrl: "https://app.acme.com/settings" });
await sandboxrouter.users.logins.session.upload("user_123", login.id, await context.storageState());
const { signedIn } = await sandboxrouter.users.logins.check("user_123", login.id);
```

`list`, `get`, `update` and `delete` read and change logins; `passkeys.rename` and `passkeys.delete` a login's passkeys; `session.get`, `session.upload` and `session.delete` its saved sign-in, whose `StorageState` is Playwright's, so `context.storageState()` passes straight in (a Browserbase login takes no upload); and `live.open`, `live.done` and `live.cancel` a live view for a person to sign in by hand. A login never carries its password or code back. `create` sends an `Idempotency-Key`, its own unless you pass `idempotencyKey`, so a retry adds the login once.

## Lists

Lists of sandboxes, MCP servers, logins and audit log entries come in pages. Await a list for one page, or iterate it for every item:

```ts
const page = await sandboxrouter.sandboxes.list({ limit: 20 });

for await (const sandbox of sandboxrouter.sandboxes.list()) console.log(sandbox.id, sandbox.labels.name);
```

## Errors and retries

Every failure is a `SandboxRouterError` with the HTTP `status`, the API's `code`, a `message` and the `requestId`, which you can quote when reporting a problem. Subclasses name the status, such as `NotFoundError`, `ConflictError` and `PaymentRequiredError`:

```ts
import { ConflictError } from "@sandboxio/sdk";

try {
  await sandboxrouter.sandboxes.exec(sandbox.id, { command: ["npm", "test"] });
} catch (error) {
  if (error instanceof ConflictError && error.code === "not_running") console.log("start it first");
  else throw error;
}
```

The SDK retries a request that is safe to send again, twice by default, after a `429`, a `5xx` or no answer. Creating a sandbox, running an exec and creating an MCP server each send an `Idempotency-Key`, so a retry never creates anything twice.

## Switch from Daytona

Code written for Daytona's TypeScript SDK (`@daytonaio/sdk`) runs on Sandbox Router as it is. Change two settings and nothing else:

```sh
export DAYTONA_API_URL=https://api.sandboxrouter.io/daytona
export DAYTONA_API_KEY=<api-key>
```

Or pass the same two values in code:

```ts
import { Daytona } from "@daytonaio/sdk";

const daytona = new Daytona({ apiUrl: "https://api.sandboxrouter.io/daytona", apiKey: process.env.SANDBOXROUTER_API_KEY });
const sandbox = await daytona.create();
const { exitCode, result } = await sandbox.process.executeCommand("echo hi");
```

The key is an API key from your workspace, as above. Set `DAYTONA_API_URL` in the real environment (your shell, CI or platform settings), not in a `.env` file: the Daytona SDK reads it only from the environment, so a `.env` value sends your key to Daytona instead. Leave off a trailing slash.

What works: creating, listing, starting, stopping and deleting sandboxes, labels, the idle stop (`autoStopInterval`, 5 to 240 minutes), network settings, `executeCommand`, uploading, downloading and deleting files, and preview links. A sandbox's `snapshot`, or `image` given by name, is an image in your workspace's registry ([Images](/docs/images)). Pick a provider with the label `sandboxrouter.io/provider`, such as `{ labels: { "sandboxrouter.io/provider": "cloudflare" } }`. `executeCommand` runs in zsh or bash when the image has one, else `sh`, as Daytona runs it, and in `/workspace` unless you pass a `cwd`, which is also where a relative file path goes. Its `result` is the command's output followed by its errors. `daytona.get` finds a sandbox by its ID or its `name`; a sandbox just created can take a moment to be found by name.

What answers an error instead: sessions, PTY, `codeRun`, git, LSP, computer use, listing or moving files, snapshots, volumes and archiving each raise a `DaytonaError` with status `501`. A setting Sandbox Router can't honour, such as `ttlMinutes`, a GPU or `autoStopInterval: 0`, raises a `DaytonaValidationError` that names it. So does `ephemeral: true`, or any `autoDeleteInterval` but `-1`: a sandbox stops when idle and stays until you delete it. `autoArchiveInterval` and `target` are accepted and have no effect. A create answers once the sandbox runs; if your images take longer than a minute to start, pass a larger timeout, such as `daytona.create(params, { timeout: 300 })`, or the SDK gives up before it learns the sandbox's ID. An upload takes at most 5,000 files at once.

## Use it from a browser

The API doesn't answer browsers' cross-origin requests, so keep the API key on your server and call the API from there.
