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
npm install @banata-boxes/sdkCreate a client
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_KEYOPENROUTER_MODEL
Recommended:
OPENROUTER_SMALL_MODEL
Example:
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.
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:
statustells you whether the sandbox can take workbrowserPreview.publicUrlis the preview link you can open in the browseragent.statustells you whether the AI agent is readyhumanHandofftells you whether a person needs to step inartifactstells you what durable outputs were saved
Browser preview state
This object describes what the preview viewer can do right now.
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:
readymeans a preview link exists and the browser is availablehandoff_requestedmeans the AI is asking a human to take overcontrolMode: "ai"means the AI is driving the browsercontrolMode: "shared"means the AI and a person can both interact during handoffcontrolMode: "human"is reserved for an explicitly human-only takeoverpublicUrlis the viewer link you can hand to a user
Paired browser state
This is the browser from the sandbox runtime point of view.
type SandboxPairedBrowser = {
sessionId?: string;
cdpUrl?: string;
previewUrl?: string;
controlMode?: "ai" | "human" | "shared";
controller?: string;
handoffRequestedAt?: number;
updatedAt?: number;
};You usually only need:
previewUrlfor the human viewercontrolModeto know who is in chargehandoffRequestedAtif 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
type SandboxAgentState = {
status: "disabled" | "booting" | "ready" | "failed";
serverBaseUrl?: string;
sessionId?: string;
defaultAgent?: "build" | "plan";
lastPromptAt?: number;
lastError?: string;
};Interpretation:
readymeans you can send promptssessionIdis useful when you later read messages or eventslastErroris 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.
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.
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:
statustells you whether work is still happeningmetadatais your app-owned context that comes back laterresultis the final agent output payloaderroris the failure reason if the task did not complete
Artifacts
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.
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:
SandboxSessionTypical response:
{
"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:
POST /v1/sandboxesclient.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:
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:
SandboxSession | nullRaw HTTP:
GET /v1/sandboxes?id=<sandboxId>client.list()
Returns:
{
items: SandboxSession[];
}Raw HTTP:
GET /v1/sandboxesBy default this returns active user sandboxes. Historical ended rows are opt-in through raw HTTP:
GET /v1/sandboxes?includeEnded=trueclient.waitForReady(id, timeoutMs)
Use this when you created a sandbox earlier and want to wait later.
Returns:
SandboxSessionIf the timeout is reached, waitTimedOut is set.
client.pause(id)
Returns:
SandboxSession | nullUse pause() when you want to keep the workspace and continue later.
client.resume(id)
Returns:
SandboxSession | nullclient.kill(id)
Returns:
SandboxSession | nullUse kill() when the work is done and you no longer need the sandbox.
Execution and file access
client.exec(id, command, args, options)
Returns:
{
stdout: string;
stderr: string;
exitCode: number;
durationMs: number;
}Raw HTTP:
POST /v1/sandboxes/execclient.runCode(id, code, { language })
Use runCode() when you want to execute a short snippet directly in the sandbox workspace.
Example:
await client.runCode(id, `
print("hello from python")
`, {
language: "python",
});Supported language values:
javascripttypescriptnodebunpythonbashshgorubyrustelixirjavadeno
Returns:
{
stdout: string;
stderr: string;
exitCode: number;
durationMs: number;
}client.fs.read(id, path)
Returns:
stringclient.fs.write(id, path, content)
Returns:
nullclient.fs.list(id, path)
Returns:
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:
{
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:
GET /v1/sandboxes/browser-preview?id=<sandboxId>client.getPreviewViewerUrl(id)
Returns:
string | nullclient.startPreview(id)
client.navigatePreview(id, url)
client.resizePreview(id, size)
These methods call the preview relay directly after resolving the preview connection.
Each returns:
{
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:
{
ok: boolean;
taskId?: string;
sessionId?: string;
waitTimedOut?: boolean;
metadata?: Record<string, JsonValue> | null;
response?: Record<string, unknown> | null;
error?: string;
}Real example:
{
"ok": true,
"taskId": "kn76kdqsemvtzdxasavnn9cjds841ktj",
"sessionId": "ses_2b7bb0e28ffelj1zzkufFqCT6x",
"response": null,
"waitTimedOut": true
}Important:
waitTimedOut: truedoes 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:
{
ok: boolean;
taskId?: string;
sessionId?: string;
metadata?: Record<string, JsonValue> | null;
response?: Record<string, unknown> | null;
error?: string;
}Example:
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_KEYOPENROUTER_MODEL
Recommended:
OPENROUTER_SMALL_MODEL
client.getAgentTask(id, taskId)
Returns:
{
ok: boolean;
task: SandboxAgentTask | null;
}Completed example:
{
"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:
{
ok: boolean;
agent: SandboxAgentState | null;
}client.listAgentMessages(id)
Returns:
{
ok: boolean;
sessionId: string | null;
messages: JsonValue;
}client.streamAgentEvents(id)
Returns a stream of event objects shaped like:
{
type: string;
data: JsonValue | string | null;
raw: string;
}Human handoff
client.getHandoff(id)
Returns:
{
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:
{
reason: "mfa" | "captcha" | "approval" | "login_failed" | "ambiguous_ui" | "file_download" | "custom";
message: string;
resumePrompt?: string;
}Returns:
{
ok: boolean;
handoff: SandboxHumanHandoffState;
preview: SandboxBrowserPreviewState | null;
pairedBrowser?: SandboxPairedBrowser | null;
}Example response:
{
"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:
{
ok: boolean;
handoff: SandboxHumanHandoffState;
preview: SandboxBrowserPreviewState | null;
pairedBrowser?: SandboxPairedBrowser | null;
}The key field after acceptance is:
handoff.status === "accepted"
Typical response:
{
"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:
{
controller?: string;
note?: string;
returnControlTo?: "ai" | "shared";
runResumePrompt?: boolean;
}Returns:
{
ok: boolean;
handoff: SandboxHumanHandoffState;
preview: SandboxBrowserPreviewState | null;
pairedBrowser?: SandboxPairedBrowser | null;
resume?: {
ok: boolean;
sessionId?: string;
response?: unknown;
};
}Important:
resumeis only present whenrunResumePromptis true and there is a stored resume prompt
Typical response when control goes back to the AI:
{
"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:
{
ok: boolean;
preview: SandboxBrowserPreviewState;
pairedBrowser: SandboxPairedBrowser | null;
}Checkpoints, artifacts, and document conversion
client.checkpoint(id, options?)
Returns:
{
ok: boolean;
status?: number;
checkpointId?: string | null;
artifacts?: SandboxArtifacts;
}client.listCheckpoints(id)
Returns:
Array<{
id: string;
createTimeMs: number;
comment?: string;
history?: string[];
}>client.restoreCheckpoint(id, checkpointId)
Returns:
{
ok: boolean;
status: number;
}client.getArtifacts(id)
Returns:
SandboxArtifacts | nullclient.getArtifactDownloadUrl(id, key, expiresInSeconds?)
Returns:
{
id: string;
key: string;
url: string;
expiresInSeconds: number;
}client.convertDocument(id, inputPath, options?)
Returns:
{
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:
{
webhookId: string;
url: string;
description?: string;
eventTypes: string[];
enabled: boolean;
signingSecret: string;
createdAt: number;
}client.listWebhooks()
Returns:
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:
{
"id": "evt_123",
"type": "sandbox.handoff.requested",
"createdAt": 1775034000000,
"orgId": "org_123",
"resource": {
"type": "sandbox",
"id": "k979hg38jvre7638mcahcs70ps83tsjb"
},
"data": {}
}Headers:
X-Banata-Event
X-Banata-Event-Id
X-Banata-Delivery-Attempt
X-Banata-Timestamp
X-Banata-SignatureHandoff webhook example
This is the part you asked about most directly.
Yes, the reason and message are included:
{
"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
{
"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.statuscan still bereadypairedBrowser.previewUrlcan 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.