# JS SDK reference The `orkestr` npm package is the official client for the sandbox API. TypeScript-first with a flat`.d.ts`. Ships dual ESM and CJS builds and works on any runtime with native `fetch` - Node 18+, Bun, Deno, Cloudflare Workers, browser. > **Get started** > > Sandboxes are free to start. Install with `npm install orkestr`, then enable them in one click from the [Sandboxes console](https://orkestr.eu/sandbox) and mint a scoped token. ## Install ```bash npm install orkestr # or pnpm add orkestr # or yarn add orkestr ``` ## Authentication Every request is authenticated with a Bearer API token. The SDK looks for it in three places, in order: - The `apiKey` option on `Sandbox.create()` etc. - A pre-built default client set via `setDefaultClient()` - The `ORKESTR_API_KEY` environment variable ```javascript import { Sandbox, OrkestrClient } from "orkestr"; // 1) Implicit: SDK reads ORKESTR_API_KEY from process.env const sbx = await Sandbox.create({ template: "python-3.12" }); // 2) Explicit: per-call apiKey const sbx2 = await Sandbox.create({ template: "python-3.12", apiKey: "ork_...", }); // 3) Reusable client (good for long-running agents) import { setDefaultClient } from "orkestr"; setDefaultClient(new OrkestrClient({ apiKey: "ork_...", baseUrl: "https://api.orkestr.eu", })); ``` > **Scoped tokens** > > Mint tokens scoped only to `sandboxes:read` and `sandboxes:write` for agent runtimes. A scoped token cannot reach the rest of the orkestr platform if it leaks. ## Sandbox class `Sandbox` is the main public class. Static methods create / fetch / list / resume sandboxes; instance methods drive exec, files, and lifecycle. All async. ### Sandbox.create ```javascript import { Sandbox } from "orkestr"; const sbx = await Sandbox.create({ template: "python-3.12", size: "small", network: "off", timeoutSeconds: 600, env: { OPENAI_API_KEY: "sk-..." }, metadata: { agentRun: "r_42" }, region: "fsn1", }); console.log(sbx.id, sbx.status); ``` Field names are camelCase. The SDK converts to snake_case on the wire so request bodies match the REST schema. | Parameter | Type | Description | | --- | --- | --- | | template required | Template | A built-in - `python-3.12`, `python-3.12-bare`, `node-22`, `debian-12`, `code-interpreter` - or a custom template id (`tmpl_...`) with your dependencies preinstalled. | | size | SandboxSize | Fixed size: "small" (1 vCPU / 1 GB), "medium" (2 vCPU / 4 GB) or "large" (4 vCPU / 8 GB). Default "small". Tier-capped (free: small; payg and enterprise: small, medium, large). | | network | NetworkMode | `"off"` (default), `"restricted"`, or `"open"`. | | timeoutSeconds | number | Sandbox auto-terminates after this many seconds. Default 600. Ceiling depends on your sandbox tier (30 min free, up to 24 h with a card on file); see Sandbox.limits(). Extend a running sandbox with sbx.setTimeout(). | | onTimeout | OnTimeout | What happens at the deadline: `"kill"` (default) terminates the sandbox; `"pause"` snapshots it so a later `Sandbox.resume()` continues where it left off. Paid tiers always pause; the free tier falls back to terminate at the snapshot cap. | | autoResume | boolean | When `true`, any exec or file call on a paused sandbox resumes it automatically first, then runs - instead of throwing - and an HTTP request to one of its public preview URLs wakes it the same way, so a parked dev server comes back on the first browser hit. Pairs with `onTimeout: 'pause'`: park at the deadline, wake on next use. The implicit resume passes the same admission gates as an explicit one. Default `false`. | | previewAuth | boolean | When `true`, this sandbox's preview URLs require an access token (the sandbox carries `previewAuthToken`); `await sbx.previewUrl(port)` builds the token-carrying URL, or send the token as the `X-Orkestr-Preview-Token` header. Default `false` keeps previews public. | | env | Record | Environment variables exposed to processes in the sandbox. | | metadata | Record | Caller-defined tags echoed back on every response. Max 16 keys, 256 chars per value. | | region | string | Region preference, e.g. `"fsn1"`. The current list is `regions` from `Sandbox.limits()`. Omit to let the control plane pick. | | allowDomains | string[] | For `network: "restricted"` only (paid plans): a custom egress allowlist that **replaces** the default set for this sandbox - still HTTPS-only and proxy-mediated. Pass bare hostnames (subdomains matched automatically) and keep any package registries you need. `Sandbox.limits()` returns the default set. Ignored for `off`/`open`. | | volume | string | Attach a persistent volume by name (or `vol_` id), mounted at `/persist`. Data written there survives terminate and returns when a later sandbox attaches the same volume - unlike `/workspace` (RAM, lost on terminate). Created on first use (default 10 GB). See [Persistent volumes](https://orkestr.eu/docs/sandboxes/js-sdk#volumes). | | apiKey | string | Override the API key for this call. Falls back to default client / env var. | | baseUrl | string | Override the API base URL for this call. | > **Custom templates** > > Pass a template id (`tmpl_...`) to boot from an image with your dependencies baked in - same call, no install at boot. The SDK consumes templates; you build them from the console, REST API, or MCP. See the [Custom templates guide](https://orkestr.eu/docs/sandboxes/templates). ### Sandbox.get and Sandbox.list ```javascript // Reattach from another process const sbx = await Sandbox.get("sbx_01HXYZ..."); // List your sandboxes (newest first) const running = await Sandbox.list({ status: "running" }); for (const sbx of running) { console.log(sbx.id, sbx.template, sbx.createdAt); } // Find the sandbox for a session by its metadata tags (AND across keys). // list() auto-pages; pass maxResults to cap. const forUser = await Sandbox.list({ metadata: { userId: "42", session: "abc" } }); ``` ### Sandbox.limits ```javascript const limits = await Sandbox.limits(); limits.plan; // "free" limits.allowedSizes; // ["small"] limits.allowedSizes.includes("medium"); // false limits.maxConcurrent; // 2 (RAM budget as a count of small boxes) limits.maxSnapshots; // 1 (paused sandboxes you may hold) limits.trialCreditEur; // 10.0 (one-time trial credit, EUR) limits.trialCreditUsedEur; // 2.5 (spent so far; the trial never resets) limits.defaultEgressDomains; // the restricted-mode default allowlist for (const r of limits.regions) { // regions a create may pin right now console.log(r.id, r.label); } for (const s of limits.sizes) { // full menu, each with .allowed console.log(s.size, s.cpu, s.memoryMb, s.allowed); } ``` `Sandbox.limits` reports the sizes and caps available to your API key's plan - pick a `size` up front instead of learning the limit from a rejected create. Useful when the same code runs under keys on different plans. ### sbx.exec Run a shell command and resolve with the buffered result. Throws `ExecTimeout` if the command exceeds `timeoutSeconds`. ```javascript const result = await sbx.exec("python /workspace/main.py", { timeoutSeconds: 60, }); result.stdout; // string result.stderr; // string result.exitCode; // number result.durationMs; // number ``` ### sbx.execStream Async iterable that yields chunks as they arrive. The final chunk carries `exitCode` and `durationMs`; chunks before it carry process output. Iterate to completion - breaking early leaves the in-VM process running until its own timeout fires. ```javascript for await (const chunk of sbx.execStream("python long_task.py")) { if (chunk.stream === "stdout") { process.stdout.write(chunk.data); } else { process.stderr.write(chunk.data); } if (chunk.exitCode !== undefined) { console.log(`exit=${chunk.exitCode} duration=${chunk.durationMs}ms`); } } ``` ### Background commands `exec(cmd, { background: true })` detaches: it resolves immediately with a `BackgroundCommand` handle while the process keeps running - a dev server, a build, a long job. The process survives pause/resume and dies with the sandbox. Handles reattach from any process via `sbx.commands()`; a command's status is `running`, `exited` (with `exitCode`) or `killed`. ```javascript // Start a dev server without nohup gymnastics: const handle = await sbx.exec("python -m http.server 8000", { background: true }); handle.pid; handle.status; // 41, "running" // Reattach from another process: for (const cmd of await sbx.commands()) { console.log(cmd.id, cmd.command, cmd.status); } (await handle.logs()).content; // buffered output so far for await (const chunk of handle.stream()) { // ...or follow it live process.stdout.write(chunk.data); } await handle.wait({ timeoutSeconds: 120 }); // block until it stops await handle.kill(); // SIGTERM the process group, then SIGKILL ``` ### sbx.runCode (code interpreter) On the `code-interpreter` template, `runCode` executes Python in a persistent kernel: variables and imports carry over between calls, and the returned `CodeExecution` carries rich `CodeResult` items (text, HTML tables, PNG charts and parsed chart data) plus a structured `CodeError` when the code raises. Timeouts interrupt the kernel but preserve its variables. On other templates it throws `InterpreterUnavailable`. Full guide: [Code interpreter](https://orkestr.eu/docs/sandboxes/code-interpreter). ```javascript const sbx = await Sandbox.create({ template: "code-interpreter" }); await sbx.runCode("import pandas as pd; df = pd.read_csv('sales.csv')"); const execution = await sbx.runCode("df.describe()"); // df persisted execution.results[0]?.text; // text rendering of the final expression execution.results[0]?.html; // ...and the HTML table execution.error; // structured CodeError on a raising cell, else null // Isolated contexts: separate interpreters inside one sandbox const ctx = await sbx.createCodeContext(); await ctx.runCode("x = 1"); // invisible to the default context ``` ### Files namespace `sbx.files` is the file operations namespace. All methods are async. Writes go to the file API's writable roots (`/workspace`, `/tmp`, and `/persist` when a volume is attached); reads work anywhere readable inside the sandbox. Shell commands via `exec` are not restricted this way. ```javascript // Text I/O await sbx.files.write("/workspace/script.py", "console.log('hi')"); const content = await sbx.files.read("/workspace/output.txt"); // Binary I/O await sbx.files.writeBytes( "/workspace/blob.bin", new Uint8Array([0, 1, 2]), ); const raw = await sbx.files.readBytes("/workspace/blob.bin"); // Directory listing for (const entry of await sbx.files.list("/workspace")) { console.log(entry.name, entry.isDir, entry.size, entry.mode); } // Directory + move + stat/exists (no shelling out) await sbx.files.mkdir("/workspace/data"); // mkdir -p by default await sbx.files.move("/workspace/a", "/workspace/data/a"); const st = await sbx.files.stat("/workspace/data/a"); // .exists is a normal check if (st.exists && !st.isDir) console.log(st.size, st.mode); await sbx.files.exists("/workspace/nope"); // false, not an error // Remove await sbx.files.delete("/workspace/output.txt"); ``` ### Batch writes and pre-signed URLs `writeBatch` writes up to 64 files in one call. Files land in order, and a failed file reports in its own result row (check `error`) without aborting the rest. For files too big for the one-shot 16 MiB cap, `presignUpload` / `presignDownload` mint expiring URLs (60 - 3600 seconds, default 600) that move bytes directly over plain HTTP - no auth header, no base64; uploads up to 512 MiB. They work for `network: "off"` sandboxes, and a paused sandbox created with `autoResume: true` wakes on first use of the URL. ```javascript // Many files in one call (scaffolding a project without N round-trips). // Per-file results: a bad path fails its own row, the rest still land. const results = await sbx.files.writeBatch({ "/workspace/src/app.py": "print('hi')", "/workspace/README.md": "# my app", }); for (const r of results) { console.log(r.path, r.bytesWritten, r.error); // error is null on success } // Large files: mint a pre-signed URL and move bytes directly - plain // HTTP, no auth header, no base64, no 16 MiB cap. Reusable until expiry. const up = await sbx.files.presignUpload("/workspace/dataset.csv", { expiresIn: 600 }); // await fetch(up.url, { method: "PUT", body: file }); const down = await sbx.files.presignDownload("/workspace/out/report.pdf"); console.log(down.url); // hand to a browser, curl, or another service ``` ### Persistent volumes `/workspace` is fast scratch, but it is RAM-backed and gone when the sandbox terminates. To keep data *across* sandboxes, attach a persistent volume - a named disk mounted at `/persist`. Anything written there survives terminate and comes back when a later sandbox attaches the same volume. Name a volume in `Sandbox.create({ volume })` and it is created on first use (default 10 GB). ```javascript import { Sandbox, Volume } from "orkestr"; // Attach a volume by name - created on first use (default 10 GB). // Anything under /persist survives terminate. let sbx = await Sandbox.create({ template: "python-3.12", volume: "my-cache" }); await sbx.exec("echo trained-weights-v1 > /persist/model.txt"); await sbx.terminate(); // A brand-new sandbox re-attaches the same volume and sees the data: sbx = await Sandbox.create({ template: "python-3.12", volume: "my-cache" }); console.log((await sbx.exec("cat /persist/model.txt")).stdout); // trained-weights-v1 // Manage volumes explicitly (custom size, a region pin, listing, cleanup): const vol = await Volume.create("datasets", { sizeGb: 50, region: "rbx" }); for (const v of await Volume.list()) console.log(v.name, v.sizeMb, v.status, v.region); await Volume.move(vol.id, "hel1"); // re-home a detached volume to another region await Volume.delete(vol.id); // rejected while attached to a running sandbox ``` A volume backs **one running sandbox at a time** (attaching a busy one throws `volume_in_use`), and attaching one pins the sandbox to the volume's host - pass the same (or no) `region`. ### Lifecycle webhooks Get POSTed instead of polling: register an endpoint with `Webhook.create` and orkestr delivers a signed JSON payload on lifecycle events - `sandbox.created` / `.paused` / `.resumed` / `.terminated` / `.expired`, `template.build_finished`, `volume.moved`. Verify every delivery with `verifyWebhookSignature` - it recomputes the HMAC over the raw body (Web Crypto, so it works in Node 18+, browsers and edge runtimes) and compares in constant time. See [the REST API reference](https://orkestr.eu/docs/sandboxes/api-reference) for the full delivery contract (headers, retries, auto-disable). ```javascript import { Webhook, verifyWebhookSignature } from "orkestr"; // Register once; store the secret - it is shown ONLY in this response. const whk = await Webhook.create("https://example.com/hooks/orkestr", { eventTypes: ["sandbox.paused", "sandbox.expired"], // omit for all events }); saveSomewhere(whk.secret); // whsec_... // In your receiver (Express shown), verify the RAW body before parsing: app.post("/hooks/orkestr", express.raw({ type: "*/*" }), async (req, res) => { const ok = await verifyWebhookSignature( secret, req.body, req.get("X-Orkestr-Signature"), ); if (!ok) return res.status(401).end(); const delivery = JSON.parse(req.body); // {event: "sandbox.paused", data: {...}} res.status(200).end(); }); await Webhook.list(); // your registrations; secret never included await Webhook.enable(whk.id); // re-arm one disabled after repeated failures await Webhook.delete(whk.id); ``` ### sbx.metrics Live CPU and memory for the sandbox - the latest reading, a rolling ~60s sample window for sparklines, and the sandbox's lifetime totals. Use it to watch a workload for saturation or memory pressure without instrumenting the workload itself. `cpu.usagePercent` is the share of allocated cores; `memory.usageBytes` is the working set (reclaimable cache excluded). ```javascript const m = await sbx.metrics(); m.sandboxStatus; // "running" (usage is null when paused / terminated) m.cpu.cores; // 1.0 m.cpu.usagePercent; // 94.0 (% of allocated cores; 1-core pegged = 100) m.cpu.usageCores; // 0.94 (cores in use) m.memory.usageBytes; // 188743680 (working set, excludes reclaimable cache) m.memory.usagePercent; // 17.6 (% of m.memory.limitBytes) m.lifetime.cpuSeconds; // 12.84 (on-CPU seconds since the sandbox started) for (const s of m.samples) { // rolling ~60s window, oldest first console.log(s.t, s.cpuPercent, s.memBytes); } ``` It is telemetry, not a state change: a paused or terminated sandbox resolves with null live usage (check `sandboxStatus`), not a throw. Pass `{ since: Date }` to fetch only samples newer than your last poll, and poll no faster than `sampleIntervalSeconds`. ### sbx.getHost Return a public hostname for an HTTP port a process in the sandbox is serving. Build the URL as ``https://${await sbx.getHost(3000)}`` and open it in a browser or embed it in your app for a live preview of a dev server running inside the sandbox. The URL is public - its only capability is the unguessable sandbox id in the hostname - stable for the sandbox's lifetime, but serves traffic only while the sandbox is running. WebSockets ride through, so dev-server HMR works. ```javascript const sbx = await Sandbox.create({ template: "node-22", network: "open" }); // Start a server inside the sandbox (here, in the background): await sbx.exec("nohup python3 -m http.server 3000 >/tmp/srv.log 2>&1 &"); const host = await sbx.getHost(3000); // "3000-01hxyz....sbx.orkestr.run" const url = `https://${host}`; // open in a browser or embed it in your app ``` Requires a card on file (paid tier) and a networked sandbox (`network: "restricted"` or `"open"`); an `off` sandbox has no port to expose. See [Networking & egress](https://orkestr.eu/docs/sandboxes/networking) for the URL format and caveats. ### sbx.setTimeout Extend a running sandbox's lifetime, measured from now (a keep-alive so a busy session is not reaped mid-task). The clock resets to run `timeoutSeconds` from the call - not added to the time remaining - and `sbx.expiresAt` updates in place. Capped by your tier's `maxTimeoutSeconds`; a paused or terminated sandbox throws. ```javascript await sbx.setTimeout(3600); // live for another hour from now console.log(sbx.expiresAt); // the new deadline ``` ### sbx.pause and Sandbox.resume `pause()` snapshots the sandbox and stops the compute meter. Works in every network mode. Returns the sandbox id - persist it wherever your agent keeps state and pass to `Sandbox.resume()` when you want to come back. A paused sandbox is reclaimed if never resumed - `sbx.pausedExpiresAt` carries the deadline (free: 7 days, card on file: 30 days, enterprise: never) and each resume restarts the clock. Resuming consumes the snapshot and restarts the sandbox's lifetime clock. If the sandbox was created with `autoResume: true` you can skip the explicit resume entirely - the next exec or file call wakes it by itself. ```javascript // Pause: returns the sandbox id, stops compute meter const sandboxId = await sbx.pause(); // ... later ... const resumed = await Sandbox.resume(sandboxId); ``` ### Sandbox.withTemp Convenience wrapper that creates a sandbox, runs your callback, and terminates the sandbox - even if the callback throws. Use this for short-lived "do one thing" loops; otherwise call `terminate()` yourself. ```javascript import { Sandbox } from "orkestr"; await Sandbox.withTemp({ template: "python-3.12" }, async (sbx) => { const result = await sbx.exec("python -c 'print(2+2)'"); console.log(result.stdout); }); // terminate() is awaited automatically on return, including on thrown errors ``` ## Result types All result types are flat objects. `ExecResult` has `stdout`, `stderr`, `exitCode`, `durationMs`. `ExecChunk` has `stream`, `data`, plus `exitCode` and `durationMs` on the final chunk only. `FileEntry` has `name`, `isDir`, `size`, `mode`, `modifiedAt` (a Date). ## Errors Every error the SDK throws extends `OrkestrError`. Use `instanceof` to dispatch. ```javascript import { Sandbox, OrkestrError, AuthError, ExecTimeout, SandboxNotFound, SnapshotCapReached, } from "orkestr"; try { const sbx = await Sandbox.create({ template: "python-3.12" }); await sbx.exec("sleep 9999", { timeoutSeconds: 5 }); } catch (err) { if (err instanceof ExecTimeout) { console.log("Command timed out; sandbox is still running."); } else if (err instanceof AuthError) { console.error(`Auth failed (${err.statusCode}): ${err.detail}`); } else if (err instanceof OrkestrError) { console.error(`Other failure: ${err.message} requestId=${err.requestId}`); } else { throw err; } } ``` | Parameter | Type | Description | | --- | --- | --- | | AuthError | OrkestrError | API key missing, invalid, expired, or scope insufficient. 401 / 403. | | RateLimitError | OrkestrError | Plan rate limit hit. `retryAfter` attribute carries the recommended wait. | | PlanLimitError | OrkestrError | Sandbox tier limit hit. | | SnapshotCapReached | PlanLimitError | pause() blocked because the snapshot retention cap is full. | | SandboxNotFound | OrkestrError | Sandbox id doesn't exist or doesn't belong to the caller. 404. | | SandboxNotReady | OrkestrError | Operation called on a sandbox in the wrong state (e.g. exec on paused). 409. | | ExecTimeout | OrkestrError | exec exceeded timeoutSeconds. | | ExecKilled | OrkestrError | Exec process exited via a signal. `signal` attribute carries the signal number. | | NetworkPolicyError | OrkestrError | In-sandbox code tried to reach a host blocked by the network policy. | | ConnectionError | OrkestrError | Transport-level failure reaching api.orkestr.eu (DNS, TCP, timeout before bytes). | ## Field naming The REST API uses `snake_case`; this SDK exposes `camelCase` to feel native to JS code. The client converts at the boundary - request bodies and query params, response fields, and error details are all rewritten for you. Field names in this reference match what you see in your IDE. ## Versioning SDK versioning follows [Semantic Versioning](https://semver.org). The SDK targets the v1 sandbox API; non-breaking additions to the API ship as SDK patch / minor bumps. A future v2 API will require an SDK major.