Banata

Reference

API Reference

Use the Banata SDK and HTTP API with confidence. This page explains the core response objects, when to use each method, and the exact fields you can expect back.

The SDK is the main integration surface.

Use this page for three things:

  • to choose the right SDK method for a job
  • to understand the JSON objects Banata returns
  • to map SDK calls to raw HTTP endpoints when you need them

If you are new to Banata, start with Quick Start first, then come back here when you need exact request and response details.

Install the SDK

bash
npm install @banata-boxes/sdk

Create a client

ts
import { BanataSandbox } from "@banata-boxes/sdk";
 
const client = new BanataSandbox({
  apiKey: process.env.BANATA_API_KEY!,
  baseUrl: "https://api.boxes.banata.dev",
});

Required env for AI agent work

If you plan to call prompt() or promptAsync(), create the sandbox with model provider settings in env.

Use:

  • OPENROUTER_API_KEY
  • OPENROUTER_MODEL

Recommended:

  • OPENROUTER_SMALL_MODEL

Example:

ts
const sandbox = await client.launch({
  env: {
    OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY!,
    OPENROUTER_MODEL: "openai/gpt-5.3-codex",
    OPENROUTER_SMALL_MODEL: "openai/gpt-5.3-codex",
  },
  capabilities: {
    agent: { enabled: true, defaultAgent: "build" },
    browser: { viewport: { width: 1440, height: 900 } },
  },
});

Without that env, the sandbox may still exist and the browser may still work, but AI agent prompts may not run correctly.

How to read this page

Banata has a few response objects that appear again and again:

  • the sandbox session
  • the browser preview state
  • the AI agent state
  • the human handoff state
  • the agent task
  • artifacts and document conversion results

Once you understand those objects, the rest of the API becomes much easier to reason about.

Core response objects

Sandbox session

This is the main object returned from sandbox creation, lookup, listing, pause, resume, and kill flows.

ts
type SandboxSession = {
  id: string;
  status:
    | "queued"
    | "assigning"
    | "ready"
    | "active"
    | "ending"
    | "ended"
    | "failed"
    | "pausing"
    | "paused";
  waitTimedOut?: boolean;
  region?: string | null;
  terminalUrl: string | null;
  previewBaseUrl?: string | null;
  capabilities?: SandboxCapabilities | null;
  pairedBrowser?: SandboxPairedBrowser | null;
  agent?: SandboxAgentState | null;
  browserPreview?: SandboxBrowserPreviewState | null;
  humanHandoff?: SandboxHumanHandoffState | null;
  artifacts?: SandboxArtifacts | null;
  createdAt: number;
  duration: number | null;
};

What matters most in practice:

  • status tells you whether the sandbox can take work
  • browserPreview.publicUrl is the preview link you can open in the browser
  • agent.status tells you whether the AI agent is ready
  • humanHandoff tells you whether a person needs to step in
  • artifacts tells you what durable outputs were saved

Browser preview state

This object describes what the preview viewer can do right now.

ts
type SandboxBrowserPreviewState = {
  status:
    | "disabled"
    | "provisioning"
    | "ready"
    | "active"
    | "handoff_requested"
    | "released"
    | "failed";
  publicUrl?: string;
  websocketUrl?: string;
  controlMode?: "ai" | "human" | "shared";
  controller?: string;
  leaseExpiresAt?: number;
  updatedAt?: number;
};

Interpretation:

  • ready means a preview link exists and the browser is available
  • handoff_requested means the AI is asking a human to take over
  • controlMode: "ai" means the AI is driving the browser
  • controlMode: "shared" means the AI and a person can both interact during handoff
  • controlMode: "human" is reserved for an explicitly human-only takeover
  • publicUrl is the viewer link you can hand to a user

Paired browser state

This is the browser from the sandbox runtime point of view.

ts
type SandboxPairedBrowser = {
  sessionId?: string;
  cdpUrl?: string;
  previewUrl?: string;
  controlMode?: "ai" | "human" | "shared";
  controller?: string;
  handoffRequestedAt?: number;
  updatedAt?: number;
};

You usually only need:

  • previewUrl for the human viewer
  • controlMode to know who is in charge
  • handoffRequestedAt if you want to measure how long a human step has been waiting

Normal control flow:

  • fresh sandbox: controlMode: "ai"
  • handoff requested or accepted: controlMode: "shared"
  • handoff completed and returned: controlMode: "ai"

AI agent state

ts
type SandboxAgentState = {
  status: "disabled" | "booting" | "ready" | "failed";
  serverBaseUrl?: string;
  sessionId?: string;
  defaultAgent?: "build" | "plan";
  lastPromptAt?: number;
  lastError?: string;
};

Interpretation:

  • ready means you can send prompts
  • sessionId is useful when you later read messages or events
  • lastError is what you inspect if prompts stop working

Human handoff state

This is the most important object if your app needs to react to login, approval, or verification steps.

ts
type SandboxHumanHandoffState = {
  requestId: string;
  status: "pending" | "accepted" | "completed" | "cancelled" | "expired";
  reason:
    | "mfa"
    | "captcha"
    | "approval"
    | "login_failed"
    | "ambiguous_ui"
    | "file_download"
    | "custom";
  message: string;
  requestedBy: "agent" | "worker" | "sdk" | "dashboard";
  controller?: string;
  resumePrompt?: string;
  note?: string;
  requestedAt: number;
  acceptedAt?: number;
  completedAt?: number;
  expiresAt?: number;
  updatedAt: number;
};

This is the object you inspect when you want to know:

  • why the workflow stopped
  • whether a human has already taken over
  • what message to show in your own UI
  • what prompt should run when control returns to the agent

Yes, the reason is available in the SDK and in the webhook payload.

Agent task

Use this object for asynchronous AI work.

ts
type SandboxAgentTask = {
  id: string;
  sandboxId: string;
  status: "queued" | "running" | "completed" | "failed";
  prompt: string;
  agent: "build" | "plan";
  sessionId?: string | null;
  metadata?: Record<string, JsonValue> | null;
  result?: JsonValue | null;
  error?: string | null;
  requestedAt: number;
  startedAt?: number | null;
  submittedAt?: number | null;
  completedAt?: number | null;
  updatedAt: number;
};

What matters most:

  • status tells you whether work is still happening
  • metadata is your app-owned context that comes back later
  • result is the final agent output payload
  • error is the failure reason if the task did not complete

Artifacts

ts
type SandboxArtifacts = {
  workspaceKey?: string;
  checkpointKey?: string;
  manifestKey?: string;
  outputLogKey?: string;
  items?: Array<{
    key: string;
    path: string;
    kind: string;
    createdAt: number;
    contentType?: string;
    sizeBytes?: number;
  }>;
  updatedAt?: number;
};

Use this to discover:

  • saved recordings
  • converted documents
  • checkpoint bundles
  • generated output files

Sandbox lifecycle

client.create(config)

Use create() when your own system wants to manage readiness explicitly.

ts
const sandbox = await client.create({
  region: "iad",
  env: {
    OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY!,
    OPENROUTER_MODEL: "openai/gpt-5.3-codex",
    OPENROUTER_SMALL_MODEL: "openai/gpt-5.3-codex",
    JOB_ID: "job_42",
  },
  capabilities: {
    agent: { enabled: true, defaultAgent: "build" },
    browser: {
      viewport: { width: 1440, height: 900 },
    },
    documents: { libreofficeHeadless: true },
  },
});

Returns:

ts
SandboxSession

Typical response:

json
{
  "id": "k972syypfcfkwm4my46hgmj85n8403kg",
  "status": "ready",
  "region": null,
  "terminalUrl": null,
  "capabilities": {
    "agent": { "enabled": true, "defaultAgent": "build" },
    "browser": {
      "viewport": { "width": 1440, "height": 900 },
      "recording": false,
      "byoProxyConfigured": false
    },
    "documents": { "libreofficeHeadless": true }
  },
  "pairedBrowser": {
    "cdpUrl": "ws://127.0.0.1:9223/devtools/browser/...",
    "previewUrl": "https://boxes.banata.dev/preview?...",
    "controlMode": "ai",
    "updatedAt": 1775035858820
  },
  "agent": {
    "status": "ready",
    "sessionId": "ses_...",
    "defaultAgent": "build"
  },
  "browserPreview": {
    "status": "ready",
    "publicUrl": "https://boxes.banata.dev/preview?...",
    "websocketUrl": "https://...sprites.app/preview/ws?...",
    "controlMode": "ai",
    "updatedAt": 1775035858820
  },
  "humanHandoff": null,
  "artifacts": null,
  "createdAt": 1775035858820,
  "duration": 2139092
}

Raw HTTP:

http
POST /v1/sandboxes

client.launch(config)

Use launch() when you want the easiest path.

It creates the sandbox and waits until it is usable, then returns a rich helper object with methods already bound to that sandbox.

Typical use:

ts
const sandbox = await client.launch({
  env: {
    OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY!,
    OPENROUTER_MODEL: "openai/gpt-5.3-codex",
    OPENROUTER_SMALL_MODEL: "openai/gpt-5.3-codex",
  },
  capabilities: {
    agent: { enabled: true },
    browser: { viewport: { width: 1440, height: 900 } },
  },
});
 
await sandbox.exec("echo", ["hello"]);
await sandbox.kill();

client.get(id)

Returns:

ts
SandboxSession | null

Raw HTTP:

http
GET /v1/sandboxes?id=<sandboxId>

client.list()

Returns:

ts
{
  items: SandboxSession[];
}

Raw HTTP:

http
GET /v1/sandboxes

By default this returns active user sandboxes. Historical ended rows are opt-in through raw HTTP:

http
GET /v1/sandboxes?includeEnded=true

client.waitForReady(id, timeoutMs)

Use this when you created a sandbox earlier and want to wait later.

Returns:

ts
SandboxSession

If the timeout is reached, waitTimedOut is set.

client.pause(id)

Returns:

ts
SandboxSession | null

Use pause() when you want to keep the workspace and continue later.

client.resume(id)

Returns:

ts
SandboxSession | null

client.kill(id)

Returns:

ts
SandboxSession | null

Use kill() when the work is done and you no longer need the sandbox.

Execution and file access

client.exec(id, command, args, options)

Returns:

ts
{
  stdout: string;
  stderr: string;
  exitCode: number;
  durationMs: number;
}

Raw HTTP:

http
POST /v1/sandboxes/exec

client.runCode(id, code, { language })

Use runCode() when you want to execute a short snippet directly in the sandbox workspace.

Example:

ts
await client.runCode(id, `
print("hello from python")
`, {
  language: "python",
});

Supported language values:

  • javascript
  • typescript
  • node
  • bun
  • python
  • bash
  • sh
  • go
  • ruby
  • rust
  • elixir
  • java
  • deno

Returns:

ts
{
  stdout: string;
  stderr: string;
  exitCode: number;
  durationMs: number;
}

client.fs.read(id, path)

Returns:

ts
string

client.fs.write(id, path, content)

Returns:

ts
null

client.fs.list(id, path)

Returns:

ts
Array<{
  name: string;
  path?: string;
  type?: string;
  size?: number;
}>

Browser preview and control

client.getPreview(id)

Use this when you want a fresh preview link plus the current handoff state.

Returns:

ts
{
  id: string;
  browserPreviewUrl: string;
  browserPreviewViewerUrl: string;
  previewBaseUrl: string | null;
  browserPreview: SandboxBrowserPreviewState | null;
  humanHandoff: SandboxHumanHandoffState | null;
  pairedBrowser: SandboxPairedBrowser | null;
  runtime: {
    preview: SandboxBrowserPreviewState | null;
    pairedBrowser: SandboxPairedBrowser | null;
    websocketUrl: string | null;
    humanHandoff: SandboxHumanHandoffState | null;
  };
}

Raw HTTP:

http
GET /v1/sandboxes/browser-preview?id=<sandboxId>

client.getPreviewViewerUrl(id)

Returns:

ts
string | null

client.startPreview(id)

client.navigatePreview(id, url)

client.resizePreview(id, size)

These methods call the preview relay directly after resolving the preview connection.

Each returns:

ts
{
  ok: boolean;
  preview?: SandboxBrowserPreviewState;
  error?: string;
}

AI agent methods

client.prompt(id, prompt, options?)

Use prompt() when:

  • you want the simplest call
  • the task is short
  • you are willing to let the SDK wait briefly for a result

Returns:

ts
{
  ok: boolean;
  taskId?: string;
  sessionId?: string;
  waitTimedOut?: boolean;
  metadata?: Record<string, JsonValue> | null;
  response?: Record<string, unknown> | null;
  error?: string;
}

Real example:

json
{
  "ok": true,
  "taskId": "kn76kdqsemvtzdxasavnn9cjds841ktj",
  "sessionId": "ses_2b7bb0e28ffelj1zzkufFqCT6x",
  "response": null,
  "waitTimedOut": true
}

Important:

  • waitTimedOut: true does not mean the task failed
  • it means the request returned before the final agent message was ready
  • you should then inspect the task or messages

client.promptAsync(id, prompt, { metadata })

Use promptAsync() when:

  • the task may run for a while
  • another system will pick up the result later
  • you want webhook-driven completion

Returns:

ts
{
  ok: boolean;
  taskId?: string;
  sessionId?: string;
  metadata?: Record<string, JsonValue> | null;
  response?: Record<string, unknown> | null;
  error?: string;
}

Example:

ts
const queued = await client.promptAsync(id, "Do the work", {
  metadata: {
    userId: "user_123",
    jobId: "job_456",
    source: "admin-panel",
  },
});

For prompt() and promptAsync() to work reliably, the sandbox should have been created with:

  • OPENROUTER_API_KEY
  • OPENROUTER_MODEL

Recommended:

  • OPENROUTER_SMALL_MODEL

client.getAgentTask(id, taskId)

Returns:

ts
{
  ok: boolean;
  task: SandboxAgentTask | null;
}

Completed example:

json
{
  "ok": true,
  "task": {
    "id": "kn76kdqsemvtzdxasavnn9cjds841ktj",
    "sandboxId": "k972syypfcfkwm4my46hgmj85n8403kg",
    "status": "completed",
    "prompt": "Open https://example.com and reply with exactly OK_DONE.",
    "agent": "build",
    "sessionId": "ses_2b7bb0e28ffelj1zzkufFqCT6x",
    "metadata": null,
    "error": null,
    "requestedAt": 1775035910000,
    "startedAt": 1775035910072,
    "submittedAt": 1775035910146,
    "completedAt": 1775035917817,
    "updatedAt": 1775035917817,
    "result": {
      "sessionId": "ses_2b7bb0e28ffelj1zzkufFqCT6x",
      "message": {
        "parts": [
          { "type": "text", "text": "OK_DONE" }
        ]
      }
    }
  }
}

client.getAgentState(id)

Returns:

ts
{
  ok: boolean;
  agent: SandboxAgentState | null;
}

client.listAgentMessages(id)

Returns:

ts
{
  ok: boolean;
  sessionId: string | null;
  messages: JsonValue;
}

client.streamAgentEvents(id)

Returns a stream of event objects shaped like:

ts
{
  type: string;
  data: JsonValue | string | null;
  raw: string;
}

Human handoff

client.getHandoff(id)

Returns:

ts
{
  id: string;
  humanHandoff: SandboxHumanHandoffState | null;
  browserPreview: SandboxBrowserPreviewState | null;
  pairedBrowser: SandboxPairedBrowser | null;
  runtime: {
    handoff: SandboxHumanHandoffState | null;
    preview: SandboxBrowserPreviewState | null;
    pairedBrowser: SandboxPairedBrowser | null;
  };
}

This is the method you call when you want to know:

  • whether the workflow is waiting for a person
  • why it stopped
  • who currently has control

client.requestHumanHandoff(id, options)

Request body:

ts
{
  reason: "mfa" | "captcha" | "approval" | "login_failed" | "ambiguous_ui" | "file_download" | "custom";
  message: string;
  resumePrompt?: string;
}

Returns:

ts
{
  ok: boolean;
  handoff: SandboxHumanHandoffState;
  preview: SandboxBrowserPreviewState | null;
  pairedBrowser?: SandboxPairedBrowser | null;
}

Example response:

json
{
  "ok": true,
  "handoff": {
    "requestId": "handoff:k970q1ha95snk1chr8wkh4cg7h841vn7",
    "status": "pending",
    "reason": "custom",
    "message": "GitHub login is ready. Please take over.",
    "requestedBy": "worker",
    "controller": "api:j577p9t6thx59g96sbtrexre2n83sf7r",
    "resumePrompt": "After the human finishes, continue from the current page and report the title and URL.",
    "requestedAt": 1775038266858,
    "updatedAt": 1775038266864
  },
  "preview": {
    "status": "handoff_requested",
    "publicUrl": null,
    "websocketUrl": null,
    "controlMode": "shared",
    "controller": "api:j577p9t6thx59g96sbtrexre2n83sf7r",
    "updatedAt": 1775038266947
  },
  "pairedBrowser": {
    "cdpUrl": "ws://127.0.0.1:9223/devtools/browser/3d524c93-cc21-4d93-bea0-0648af798431",
    "previewUrl": null,
    "controlMode": "shared",
    "controller": "api:j577p9t6thx59g96sbtrexre2n83sf7r",
    "handoffRequestedAt": 1775038266858,
    "updatedAt": 1775038266947
  }
}

client.acceptHumanHandoff(id, { controller })

Returns:

ts
{
  ok: boolean;
  handoff: SandboxHumanHandoffState;
  preview: SandboxBrowserPreviewState | null;
  pairedBrowser?: SandboxPairedBrowser | null;
}

The key field after acceptance is:

  • handoff.status === "accepted"

Typical response:

json
{
  "ok": true,
  "handoff": {
    "requestId": "handoff:k970q1ha95snk1chr8wkh4cg7h841vn7",
    "status": "accepted",
    "reason": "custom",
    "message": "GitHub login is ready. Please take over.",
    "requestedBy": "worker",
    "controller": "preview-link",
    "resumePrompt": "After the human finishes, continue from the current page and report the title and URL.",
    "requestedAt": 1775038266858,
    "acceptedAt": 1775038300000,
    "updatedAt": 1775038300000
  },
  "preview": {
    "status": "handoff_requested",
    "publicUrl": null,
    "websocketUrl": null,
    "controlMode": "shared",
    "controller": "preview-link",
    "updatedAt": 1775038300000
  },
  "pairedBrowser": {
    "cdpUrl": "ws://127.0.0.1:9223/devtools/browser/3d524c93-cc21-4d93-bea0-0648af798431",
    "previewUrl": null,
    "controlMode": "shared",
    "controller": "preview-link",
    "handoffRequestedAt": 1775038266858,
    "updatedAt": 1775038300000
  }
}

client.completeHumanHandoff(id, options)

Use this when the human is done and control should go back.

Request body:

ts
{
  controller?: string;
  note?: string;
  returnControlTo?: "ai" | "shared";
  runResumePrompt?: boolean;
}

Returns:

ts
{
  ok: boolean;
  handoff: SandboxHumanHandoffState;
  preview: SandboxBrowserPreviewState | null;
  pairedBrowser?: SandboxPairedBrowser | null;
  resume?: {
    ok: boolean;
    sessionId?: string;
    response?: unknown;
  };
}

Important:

  • resume is only present when runResumePrompt is true and there is a stored resume prompt

Typical response when control goes back to the AI:

json
{
  "ok": true,
  "handoff": {
    "requestId": "handoff:k970q1ha95snk1chr8wkh4cg7h841vn7",
    "status": "completed",
    "reason": "custom",
    "message": "Human handoff requested",
    "requestedBy": "worker",
    "requestedAt": 1775042985169,
    "acceptedAt": 1775042985169,
    "completedAt": 1775042985169,
    "updatedAt": 1775042985169
  },
  "preview": {
    "status": "released",
    "publicUrl": null,
    "websocketUrl": null,
    "controlMode": "ai",
    "updatedAt": 1775042985235
  },
  "pairedBrowser": {
    "cdpUrl": "ws://127.0.0.1:9223/devtools/browser/3d524c93-cc21-4d93-bea0-0648af798431",
    "previewUrl": null,
    "controlMode": "ai",
    "updatedAt": 1775042985235
  }
}

client.setControl(id, mode)

Returns:

ts
{
  ok: boolean;
  preview: SandboxBrowserPreviewState;
  pairedBrowser: SandboxPairedBrowser | null;
}

Checkpoints, artifacts, and document conversion

client.checkpoint(id, options?)

Returns:

ts
{
  ok: boolean;
  status?: number;
  checkpointId?: string | null;
  artifacts?: SandboxArtifacts;
}

client.listCheckpoints(id)

Returns:

ts
Array<{
  id: string;
  createTimeMs: number;
  comment?: string;
  history?: string[];
}>

client.restoreCheckpoint(id, checkpointId)

Returns:

ts
{
  ok: boolean;
  status: number;
}

client.getArtifacts(id)

Returns:

ts
SandboxArtifacts | null

client.getArtifactDownloadUrl(id, key, expiresInSeconds?)

Returns:

ts
{
  id: string;
  key: string;
  url: string;
  expiresInSeconds: number;
}

client.convertDocument(id, inputPath, options?)

Returns:

ts
{
  ok: boolean;
  inputPath: string;
  outputPath: string;
  format: string;
  artifact: SandboxArtifactItem | null;
  artifacts: SandboxArtifacts | null;
}

Usage, billing, and webhooks

client.getUsage()

Returns the current period usage summary for the authenticated organization.

client.getBilling()

Returns the current billing and plan information for the authenticated organization.

client.createCheckout({ plan })

Use this when you want to start an upgrade flow from your own app.

client.createWebhook({ url, eventTypes, description })

Returns:

ts
{
  webhookId: string;
  url: string;
  description?: string;
  eventTypes: string[];
  enabled: boolean;
  signingSecret: string;
  createdAt: number;
}

client.listWebhooks()

Returns:

ts
Array<{
  id: string;
  url: string;
  description?: string;
  eventTypes: string[];
  enabled: boolean;
  lastDeliveredAt?: number;
  lastFailureAt?: number;
  lastFailureMessage?: string;
  createdAt: number;
  updatedAt: number;
}>

client.listWebhookDeliveries(limit?)

Returns recent delivery attempts.

client.testWebhook(id)

Sends a test event to the webhook target.

Webhook payload shape

Every webhook delivery uses the same top-level structure:

json
{
  "id": "evt_123",
  "type": "sandbox.handoff.requested",
  "createdAt": 1775034000000,
  "orgId": "org_123",
  "resource": {
    "type": "sandbox",
    "id": "k979hg38jvre7638mcahcs70ps83tsjb"
  },
  "data": {}
}

Headers:

text
X-Banata-Event
X-Banata-Event-Id
X-Banata-Delivery-Attempt
X-Banata-Timestamp
X-Banata-Signature

Handoff webhook example

This is the part you asked about most directly.

Yes, the reason and message are included:

json
{
  "id": "evt_123",
  "type": "sandbox.handoff.requested",
  "createdAt": 1775034000000,
  "orgId": "org_123",
  "resource": {
    "type": "sandbox",
    "id": "k979hg38jvre7638mcahcs70ps83tsjb"
  },
  "data": {
    "handoff": {
      "state": "requested",
      "reason": "approval",
      "message": "GitHub login is ready. Please take over and continue manually.",
      "resumePrompt": "After the human finishes, resume from the current page and confirm the title and URL.",
      "requestedAt": 1775034000000
    }
  }
}

Agent task completion webhook example

json
{
  "id": "evt_456",
  "type": "sandbox.agent.task.completed",
  "createdAt": 1775035000000,
  "orgId": "org_123",
  "resource": {
    "type": "sandbox",
    "id": "k972syypfcfkwm4my46hgmj85n8403kg"
  },
  "data": {
    "taskId": "kn76kdqsemvtzdxasavnn9cjds841ktj",
    "sessionId": "ses_2b7bb0e28ffelj1zzkufFqCT6x",
    "status": "completed",
    "metadata": {
      "userId": "user_123",
      "jobId": "job_456"
    },
    "result": {
      "sessionId": "ses_2b7bb0e28ffelj1zzkufFqCT6x"
    }
  }
}

Why a preview can be white even when the browser is alive

This is a real distinction:

  • the sandbox browser can be alive and already on the target page
  • while the viewer canvas fails to paint the latest framebuffer

That means:

  • browserPreview.status can still be ready
  • pairedBrowser.previewUrl can still be valid
  • browser URL and title checks can prove the page changed
  • but the live viewer can still look blank because the visual stream did not paint correctly

So if you ever see:

  • a white preview
  • but getPreview() says the browser is ready
  • and the browser URL/title are correct

then the issue is in the live preview rendering path, not in the browser task itself.