PandaStack

TypeScript SDK

@pandastack/sdk — official TypeScript / JavaScript SDK. Full reference.

The @pandastack/sdk package is the recommended way to use PandaStack from Node, Bun, Deno, or modern browsers. It targets ES2022, uses native fetch, and ships full TypeScript types.

Install

npm install @pandastack/sdk
# or: pnpm add @pandastack/sdk · yarn add @pandastack/sdk · bun add @pandastack/sdk

Configure

export PANDASTACK_API_KEY=pds_abc123def456...
export PANDASTACK_API=https://api.pandastack.ai   # optional, this is the default

On Node, the SDK also reads ~/.config/pandastack/config.json (token / apiKey / api_key) if no env vars are set.

Imports

import {
  Client,
  PandaStackError, AuthError, NotFoundError, BadRequestError, ServerError, NetworkError,
  type SandboxCreateOptions, type SandboxInfo, type ExecResult,
  type LifecycleInfo, type PreviewInfo, type SnapshotInfo,
  type TemplateInfo, type TemplateBuild, type TemplateBuildOptions,
  type TokenInfo, type CreatedToken, type MeInfo,
  type LogsOptions,
} from "@pandastack/sdk";

There is no global Sandbox class — you create one through a Client:

const client = new Client();        // reads env vars
const sandbox = await client.sandboxes.create({ template: "code-interpreter" });

Client

new Client(config?: {
  apiUrl?: string;       // default: process.env.PANDASTACK_API ?? "https://api.pandastack.ai"
  apiKey?: string;       // default: process.env.PANDASTACK_API_KEY
  timeoutMs?: number;    // default: 30_000
})

Exposes resource namespaces — client.sandboxes, client.templates, client.tokens, client.databases, client.volumes — and one direct call:

const me = await client.me();         // → { user_id, email, org_id, ... }

Sandboxes

client.sandboxes.create(opts) -> Promise<Sandbox>

const sandbox = await client.sandboxes.create({
  template:   "code-interpreter",   // default "ubuntu-24.04"; CPU + memory are baked in
  ttlSeconds: 3600,                 // default server-side (3600)
  persistent: false,
  fromSnapshot: "snap-…",           // boot from a specific snapshot
  metadata: { job: "demo" },
  volumes: [{ name: "data", readOnly: true }],
});

cpu and memoryMb (and memory_mb) are accepted for backwards compatibility but deprecated and ignored — the server always uses the template's baked size. Passing them logs a one-time console.warn. To run at a different size, bake a custom template with --cpu / --memory-mb.

Returns a Sandbox with .id, .template, .cpu, .memory_mb, .status, .guest_ip, .boot_ms, .boot_mode, .metadata, .created_at.

Other namespace calls

await client.sandboxes.list();             // Sandbox[]
await client.sandboxes.get(id);            // Sandbox
await client.sandboxes.delete(id);         // void

Sandbox methods

Exec and code

const result = await sandbox.exec("python3 -c 'print(2+2)'", { timeoutSeconds: 10 });
// { stdout: "4\n", stderr: "", exit_code: 0, exitCode: 0, duration_ms: 23 }

await sandbox.runCode("print('hi')", "python");
await sandbox.runCode("ls /workspace");                    // language defaults to "shell"
await sandbox.commands.run("ls /workspace");               // alias for exec

cmd is a string, not a list — it runs under sh -c. See Exec.

Streaming logs

for await (const line of sandbox.logs({ stream: "both", follow: true })) {
  console.log(line);
  if (line.includes("Ready")) break;
}

stream"stdout" | "stderr" | "both". follow: true opens an SSE stream until you break or the sandbox exits.

Filesystem

await sandbox.filesystem.write("/workspace/in.txt", "hi\n");
const text = await sandbox.filesystem.read("/workspace/in.txt");   // string
await sandbox.filesystem.upload("./local.csv", "/workspace/data.csv");
await sandbox.filesystem.download("/workspace/out.csv", "./out.csv");

Full guide: Filesystem.

Lifecycle

await sandbox.pause();
await sandbox.resume();

const snap   = await sandbox.snapshot();                 // SnapshotInfo
const child  = await sandbox.fork({ metadata: { branch: "exp" } });
const fanout = await sandbox.forkTree({ count: 8, metadata: { batch: "search" } });
await child.promote();                                   // detach from parent

await sandbox.hibernate();
await sandbox.wake();

await sandbox.setTtl(7200);
await sandbox.setPersistent(true);
await sandbox.lifecycle();   // { ttl_seconds, persistent, idle_seconds }

await sandbox.kill();

Sandbox implements AsyncDisposable:

{
  await using sandbox = await client.sandboxes.create({ template: "code-interpreter" });
  await sandbox.exec("python3 train.py");
}  // sandbox.kill() auto-called

Preview URLs

sandbox.previewUrl(3000);                                  // → "https://3000-…"
await sandbox.previewUrls();                               // → { 3000: "..." }

Full guide: Preview URLs.

Templates

await client.templates.list();
await client.templates.get("code-interpreter");
await client.templates.delete("my-template");

const build = await client.templates.build({
  name:     "my-template",
  rootfs:   "/path/to/Dockerfile.tar",   // string | Blob | Uint8Array | ArrayBuffer
  sizeMb:   2048,
  kernel:   "kernel-6.1",                // optional
});
console.log(build.id, build.status);

await client.templates.builds();
await client.templates.buildStatus(build.id);

TemplateInfo has name, rootfs_path, size_bytes, meta. TemplateBuild has id, name, status, error, started_at, ended_at, size_mb, bytes.

Databases

Managed PostgreSQL 16 in dedicated Firecracker microVMs (Beta). See Databases.

// Create — blocks until Postgres is accepting connections (~30–90s)
const db = await client.databases.create({ label: "my-app-db" }); // size is fixed by the postgres-16 template
console.log(db.connection_url);                  // postgres://...

await client.databases.list();                   // metadata only (no credentials)
await client.databases.get(db.id);               // full record incl. connection_url
await client.databases.connection(db.id);        // { connection_url, broker_url, broker_token }
await client.databases.delete(db.id);            // irreversible

DatabaseInfo has id, status, template, host, port, database, username, password, connection_url, broker_url, broker_token. Connect with any driver using connection_url (TLS required, ?sslmode=require).

Volumes

Persistent named volumes — ext4 block devices that survive sandbox deletion and attach at create time. See Volumes.

const vol = await client.volumes.create({ name: "models", size_mb: 4096 });
await client.volumes.list();             // VolumeInfo[]
await client.volumes.get("models");
await client.volumes.delete("models");   // refused (409) while attached to a running sandbox

// Attach at create — appears as /dev/vdb (then vdc, ...) inside the guest
const sandbox = await client.sandboxes.create({
  template: "code-interpreter",
  volumes: [{ name: "models", readOnly: true }, { name: "scratch" }],
});
await sandbox.exec("mkdir -p /mnt/models && mount -o ro /dev/vdb /mnt/models");

VolumeInfo has name, size_mb, created_at. Quotas are per plan (Free 1 × 1 GiB · Pro 10 × 10 GiB · Team 50 × 50 GiB · Enterprise unlimited; 64 GiB hard per-volume ceiling). Exceeding the quota returns a 429 (BadRequestError); storage above your plan's included total bills at $0.15 per provisioned GiB-month. Live rates and per-tier limits: GET /v1/pricing.

Apps (git-driven hosting)

Connect a GitHub repo and serve it behind a stable per-app URL with blue-green deploys. See Apps overview.

// Create — only name + git_url are required; framework is auto-detected when omitted
const app = await client.apps.create({
  name: "my-site",
  git_url: "https://github.com/acme/my-site",
  git_branch: "main",                 // optional (default: main)
  framework: "nextjs",                // optional: nextjs | vite | node | static
  install_command: undefined,         // optional build/run overrides
  build_command: undefined,
  start_command: undefined,
  root_directory: undefined,          // optional sub-dir to build from
  port: 3000,                         // optional (default: 3000)
  env: { NODE_ENV: "production" },
});
console.log(app.id, app.url);         // `url` is populated once running

await client.apps.list();             // AppInfo[]
await client.apps.get(app.id);        // includes the stable `url`
await client.apps.update(app.id, { build_command: "npm run build:prod" });
await client.apps.delete(app.id);     // also tears down the runtime sandbox

Deploys are blue-green: a fresh sandbox builds the new commit, health-checks, then the app atomically flips to it.

const dep = await client.apps.deploy(app.id, { gitRef: "v1.2.0" });  // gitRef optional
await client.apps.deployments(app.id);             // newest first
await client.apps.deployment(app.id, dep.id);      // includes full build log

for await (const line of client.apps.deployLogs(app.id, dep.id)) {   // SSE stream
  console.log(line);
}

await client.apps.rollback(app.id);   // rebuild + flip back to the previous deployment

Apps run on the base template snapshot, so CPU/RAM are governed by that template's baked size (2 GiB), not the per-app cpu/memory_mb — those are accepted for forward-compat but the agent overrides them to the snapshot size.

Private repos and push-triggered auto-deploys use a GitHub App installation; the connect flow is browser-based (GET /v1/github/connect). See GitHub integration.

Tokens

await client.tokens.list();                       // TokenInfo[]
const created = await client.tokens.create("ci-pipeline");  // CreatedToken (.token visible once)
await client.tokens.revoke(created.prefix);

Errors

Every SDK exception inherits from PandaStackError with .statusCode and .response:

ClassWhen
AuthError401 / 403 — bad / missing token.
NotFoundError404 — sandbox / template / snapshot doesn't exist.
BadRequestErrorOther 4xx — bad payload, quota exceeded.
ServerError5xx — orchestrator transient. Usually safe to retry.
NetworkErrorUnderlying fetch failure (DNS, TCP, TLS).
try {
  const s = await client.sandboxes.get(id);
} catch (e) {
  if (e instanceof NotFoundError) {
    return client.sandboxes.create({ template: "code-interpreter" });
  }
  throw e;
}

Self-hosting and preview hosts

If you're not on api.pandastack.ai, the preview URL formatter needs to know what hostname suffix to use. By default it strips api. from your apiUrl. Override:

const client = new Client({ apiUrl: "https://api.acme.dev" });
(client as any).previewHost = "preview.acme.dev";
sandbox.previewUrl(3000);  // → https://3000-{id}.preview.acme.dev

Browser usage

The SDK works in modern browsers too — every call is fetch, no Node-only APIs in the hot path. filesystem.upload / download (which use node:fs) and tokens config-file fallback are no-ops in the browser; everything else is identical. Use previewUrl(port) to get a shareable public URL for a sandbox port without leaking your API key.

On this page