JS SDK

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 and mint a scoped token.

Install

terminal
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
auth.ts
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.

ParameterTypeDescription
template requiredTemplateA 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.
sizeSandboxSizeFixed 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).
networkNetworkMode"off" (default), "restricted", or "open".
timeoutSecondsnumberSandbox 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().
onTimeoutOnTimeoutWhat 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.
autoResumebooleanWhen 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.
previewAuthbooleanWhen 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.
envRecord<string, string>Environment variables exposed to processes in the sandbox.
metadataRecord<string, string>Caller-defined tags echoed back on every response. Max 16 keys, 256 chars per value.
regionstringRegion preference, e.g. "fsn1". The current list is regions from Sandbox.limits(). Omit to let the control plane pick.
allowDomainsstring[]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.
volumestringAttach 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.
apiKeystringOverride the API key for this call. Falls back to default client / env var.
baseUrlstringOverride 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.

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.

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 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 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;
  }
}
ParameterTypeDescription
AuthErrorOrkestrErrorAPI key missing, invalid, expired, or scope insufficient. 401 / 403.
RateLimitErrorOrkestrErrorPlan rate limit hit. retryAfter attribute carries the recommended wait.
PlanLimitErrorOrkestrErrorSandbox tier limit hit.
SnapshotCapReachedPlanLimitErrorpause() blocked because the snapshot retention cap is full.
SandboxNotFoundOrkestrErrorSandbox id doesn't exist or doesn't belong to the caller. 404.
SandboxNotReadyOrkestrErrorOperation called on a sandbox in the wrong state (e.g. exec on paused). 409.
ExecTimeoutOrkestrErrorexec exceeded timeoutSeconds.
ExecKilledOrkestrErrorExec process exited via a signal. signal attribute carries the signal number.
NetworkPolicyErrorOrkestrErrorIn-sandbox code tried to reach a host blocked by the network policy.
ConnectionErrorOrkestrErrorTransport-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. 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.

Reading with an agent? This page is also plain markdown at /docs/sandboxes/js-sdk.md, and the full docs index lives at /docs/llms.txt.