TypeScript SDK#
@dormo/sdk is written by hand against the OpenAPI spec: request and response types mirror the spec's schemas, with a thin layer on top for sandbox handles, streaming, retries and webhook verification. It has no runtime dependencies and runs on Node.js 20+, Bun, Deno and edge runtimes with fetch.
Runtime notes:
files.upload(...)withFileobjects needs a globalFile(Node.js 20+, Bun, Deno, browsers).pty.connect()needs a globalWebSocket(Node.js 22+, Bun, Deno, browsers). On Node.js 20, installwsand pass it in (see PTY).verifyWebhookusesnode:crypto(Node.js, Bun, Deno). On edge runtimes use the asyncverifyWebhookAsync(Web Crypto).
npm install @dormo/sdk
Client#
import { Client } from "@dormo/sdk";
const client = new Client({
apiKey: process.env.DORMO_API_KEY, // default: this env var
baseUrl: "https://api.dormo.dev/v1", // default
maxRetries: 2, // retries errors with retryable: true
timeoutMs: 120_000,
});
Naming: methods are camelCase; request and response fields are snake_case, exactly as in the API (auto_pause, timeout_seconds, exit_code).
Sandboxes#
const sandbox = await client.sandboxes.create({
template: "base", // or snapshot_id, or image: { ref: "ghcr.io/acme/env:1" }
size: "default", // small | default | large
name: "user-42",
labels: { user: "42" },
env: { NODE_ENV: "production" },
auto_pause: { enabled: true, idle_timeout_seconds: 300 },
always_on: false,
priority: "standard", // or "batch" (−50%, may queue)
ttl_seconds: null,
ports: [{ port: 3000, visibility: "private" }],
wait: true,
}, { idempotencyKey: "user-42-create" });
sandbox.id; // "sbx_..."
sandbox.state; // "running"
const same = await client.sandboxes.get(sandbox.id);
for await (const s of client.sandboxes.list({ state: ["running"], label: ["user=42"] })) {
console.log(s.id, s.state);
}
await sandbox.update({ auto_pause: { enabled: true, idle_timeout_seconds: 900 } });
await sandbox.update({ always_on: true });
await sandbox.pause(); // waits until "paused"
await sandbox.resume(); // usually unnecessary: calls wake it
const fork = await sandbox.fork({ name: "user-42-try-2" });
const forks = await sandbox.fork({ count: 10 }); // Sandbox[]: 10 copies in one call
await sandbox.refresh(); // re-read state
await sandbox.waitFor("running", { timeoutMs: 30_000 });
await sandbox.delete();
| Method | Endpoint |
|---|---|
client.sandboxes.create(params, opts?) |
POST /sandboxes |
client.sandboxes.get(id) |
GET /sandboxes/{id} |
client.sandboxes.list(params?) |
GET /sandboxes (auto-paginates) |
sandbox.update(params) |
PATCH /sandboxes/{id} |
sandbox.pause() / sandbox.resume() |
POST /sandboxes/{id}/pause / resume |
sandbox.fork(params?) |
POST /sandboxes/{id}/fork |
sandbox.forkMany(count, params?) |
POST /sandboxes/{id}/fork with count, always returns Sandbox[] |
sandbox.delete() |
DELETE /sandboxes/{id} |
Bulk fork#
fork({ count }) creates up to 50 copies of a sandbox (memory included) in one call. fork() and fork({ count: 1 }) return one Sandbox, as before; with count > 1 the API returns { "data": [...] } and the SDK returns Sandbox[]. With a literal count (for example { count: 10 }) the return type is Sandbox[]; when count is a number variable it is Sandbox | Sandbox[], so use forkMany(count) to always get an array.
Admission is all-or-nothing: if the source host cannot fit every fork, nothing is created and the call throws ApiError with capacity_unavailable (or concurrency_limit_reached if the plan's limit would be exceeded). With priority: "batch" the forks are queued instead and come back in state queued.
const rollouts = await sandbox.forkMany(32, { labels: { run: "eval-7" } });
const results = await Promise.all(rollouts.map((s) => s.exec({ command: "python3 rollout.py" })));
const queued = await sandbox.forkMany(50, { priority: "batch", wait: false }); // -50%, may queue
Exec#
// Sync
const r = await sandbox.exec({ command: "npm test", cwd: "/home/user/app", timeout_seconds: 300 });
console.log(r.exit_code, r.stdout, r.stderr, r.truncated);
// Without a shell
await sandbox.exec({ args: ["python3", "-c", "print(42)"] });
// Stream (SSE)
for await (const ev of sandbox.execStream({ command: "pip install -r requirements.txt" })) {
if (ev.type === "stdout" || ev.type === "stderr") process.stdout.write(ev.data);
if (ev.type === "exit") console.log("exit", ev.exit_code);
}
// Background
const cmd = await sandbox.exec({ command: "npm run dev", background: true });
for await (const ev of cmd.stream()) { /* attach, replays earlier output */ }
await cmd.writeStdin("q\n", { eof: true });
await cmd.kill({ signal: "SIGTERM" });
const done = await cmd.wait(); // Command with exit_code
await sandbox.commands.list();
await sandbox.commands.get(cmd.id);
PTY#
const term = await sandbox.pty.connect({
cols: 120, rows: 40,
onData: (bytes) => process.stdout.write(bytes),
onExit: (code) => console.log("exit", code),
});
term.write("ls -la\n");
term.resize(160, 48);
term.close();
On Node.js 20, pass a WebSocket implementation: import WebSocket from "ws" and add WebSocket to the pty.connect({ ... }) options.
sandbox.pty.create() returns the raw { id, websocket_url, expires_at } if you want to connect from a browser (for example with xterm.js). Protocol: see Exec and files.
Files#
await sandbox.files.write("/home/user/app/.env", "PORT=3000\n", { mode: "0600" });
const text = await sandbox.files.readText("/home/user/app/package.json");
const bytes = await sandbox.files.read("/home/user/out.png"); // Uint8Array
const entries = await sandbox.files.list("/home/user/app", { recursive: false });
const info = await sandbox.files.stat("/home/user/app");
await sandbox.files.mkdir("/home/user/data");
await sandbox.files.move("/home/user/a.txt", "/home/user/b.txt");
await sandbox.files.remove("/home/user/tmp", { recursive: true });
await sandbox.files.upload("/home/user/app", [new File(["hi"], "src/hello.txt")]);
const tgz = await sandbox.files.downloadArchive("/home/user/app", { format: "tar.gz" });
const { url } = await sandbox.files.downloadUrl({ path: "/home/user/big.bin" });
const up = await sandbox.files.uploadUrl({ path: "/home/user/dataset.parquet", expires_in_seconds: 900 });
for await (const ev of sandbox.files.watch("/home/user/app/src", { recursive: true })) {
console.log(ev.type, ev.path);
}
Networking#
const port = await sandbox.ports.expose({ port: 3000, visibility: "private" });
port.url; // https://<short_id>-3000.dormo.host
const { token } = await sandbox.accessTokens.create({ ttl_seconds: 3600, ports: [3000] });
await sandbox.ports.list();
await sandbox.ports.remove(3000);
const ssh = await sandbox.ssh({ public_key: "ssh-ed25519 AAAA...", ttl_seconds: 3600 });
ssh.command; // ssh sbx_...@ssh.dormo.host
Desktop#
const stream = await sandbox.desktop.stream({ ttl_seconds: 600, read_only: true });
stream.url; // embed in an <iframe>
await sandbox.desktop.revoke(stream.id);
const shot = await sandbox.desktop.screenshot({ format: "png" }); // { data (base64), media_type, width, height, ... }
await sandbox.desktop.input({
actions: [
{ action: "left_click", coordinate: [640, 412] },
{ action: "type", text: "hello" },
{ action: "key", text: "Return" },
],
return_screenshot: true,
});
const { x, y } = await sandbox.desktop.cursor();
See Desktop and computer use for a full agent loop.
Snapshots and templates#
const snap = await sandbox.snapshots.create({ name: "after-npm-install" });
const copy = await client.sandboxes.create({ snapshot_id: snap.id });
await client.snapshots.list({ sandbox_id: sandbox.id });
await client.snapshots.delete(snap.id);
const tpl = await client.templates.create({
name: "agent-env",
image: { ref: "ghcr.io/acme/agent-env:1.4" },
ready_check: { command: "curl -sf localhost:8080/health", timeout_seconds: 120 },
});
await client.templates.logs(tpl.id);
await client.sandboxes.create({ template: "agent-env" });
Webhooks, usage, keys, account#
const hook = await client.webhooks.create({
url: "https://example.com/hooks/dormo",
events: ["sandbox.ready", "sandbox.paused", "sandbox.resumed", "sandbox.error", "sandbox.deleted"],
});
hook.secret; // whsec_... (shown once)
import { verifyWebhook, verifyWebhookAsync } from "@dormo/sdk/webhooks";
const event = verifyWebhook({ payload: rawBody, headers: req.headers, secret: process.env.WEBHOOK_SECRET! });
// Edge runtimes (Cloudflare Workers, Vercel Edge): Web Crypto is async-only
const edgeEvent = await verifyWebhookAsync({ payload: rawBody, headers: req.headers, secret: env.WEBHOOK_SECRET });
const usage = await client.usage.get({ group_by: "sandbox" });
const limits = await client.limits.get();
const key = await client.apiKeys.create({ name: "user-42", preset: "full-sandbox", restrict: { labels: { user: "42" } } });
await client.account.setSpendLimit({ monthly_limit_usd: 500, on_limit: "pause_running" });
Errors#
Every API error becomes an ApiError:
import { ApiError } from "@dormo/sdk";
try {
await client.sandboxes.create({ size: "large" });
} catch (err) {
if (err instanceof ApiError) {
console.log(err.status, err.code, err.retryable, err.requestId, err.details);
if (err.code === "concurrency_limit_reached") { /* pause something first */ }
} else throw err;
}
Errors with retryable: true (rate_limited, capacity_unavailable, sandbox_busy, ...) are retried automatically up to maxRetries, honouring Retry-After.