Reference
Webhooks
Use webhook callbacks to track long-running sandbox and AI agent work without holding open requests or relying on constant polling.
Webhooks are the best integration tool for long-running jobs.
They let your app react when important work finishes instead of:
- keeping one request open
- polling constantly
- guessing whether the job is probably done
When you should use webhooks
Use webhooks when:
- an AI agent task may run for a while
- your backend should continue work only after Banata finishes
- another system should receive a callback later
The normal webhook flow
- create a webhook endpoint in your own app
- register that endpoint with Banata
- queue agent work with
promptAsync() - pass your own metadata with the task
- let Banata notify you when the job finishes
- read files or download artifacts afterward
Create a webhook
const webhook = await client.createWebhook({
url: "https://your-app.example.com/banata/webhook",
eventTypes: ["sandbox.agent.task.completed"],
description: "Task completion receiver",
});Exact response shape:
{
"id": "kh7ev9z670rc9fk1gx7mqtdm09841zrj",
"url": "https://your-app.example.com/banata/webhook",
"description": "Task completion receiver",
"eventTypes": [
"sandbox.agent.task.completed"
],
"enabled": true,
"signingSecret": "2ca3941580ae81eb9769c91b31770b2646acbf0c41538a01",
"createdAt": 1775038321611
}Pass metadata with the task
const queued = await sandbox.promptAsync(
"Generate a report and save it to /workspace/report.pdf",
{
metadata: {
userId: "user_123",
jobId: "job_456",
source: "customer-portal",
},
},
);Useful event types
Common events you may want:
sandbox.createdsandbox.assignedsandbox.readysandbox.pausedsandbox.resumedsandbox.endedsandbox.failedsandbox.agent.readysandbox.agent.failedsandbox.browser.readysandbox.browser.failedsandbox.agent.task.queuedsandbox.agent.task.completedsandbox.agent.task.failedsandbox.checkpoint.createdsandbox.checkpoint.restoredsandbox.document.convertedsandbox.artifacts.updatedbilling.subscription.updatedbilling.plan.changed
Signatures and retries
Webhook deliveries are signed and retried automatically.
Your receiver should:
- verify the signature
- return
2xxwhen it accepts the event - make processing idempotent
Webhook payload shape
Every webhook uses the same top-level envelope:
{
"id": "evt_123",
"type": "sandbox.agent.task.completed",
"createdAt": 1775035000000,
"orgId": "org_123",
"resource": {
"type": "sandbox",
"id": "k972syypfcfkwm4my46hgmj85n8403kg"
},
"data": {}
}The part you usually inspect is data.
Handoff event example
If the AI asks a person to step in, the webhook includes the handoff reason and message.
This is the exact payload shape from a real delivery:
{
"id": "69fea4ee-f8c5-460d-9b87-fcea6d0bd5ab",
"type": "sandbox.handoff.requested",
"createdAt": 1775038266864,
"orgId": "org_test_1",
"resource": {
"type": "sandbox",
"id": "k970q1ha95snk1chr8wkh4cg7h841vn7"
},
"data": {
"sandboxId": "k970q1ha95snk1chr8wkh4cg7h841vn7",
"state": "ready",
"spriteName": "banata-sbx-system-warm--k970q1ha95snk1chr8wk",
"region": null,
"updatedAt": 1775038266864,
"handoff": {
"state": "requested",
"reason": "custom",
"message": "GitHub login is ready. Please take over.",
"resumePrompt": "After the human finishes, continue from the current page and report the title and URL.",
"requestedAt": 1775038266858
}
}
}So if your app needs to react differently to:
captchamfaapprovalambiguous_ui
you can do that directly from data.handoff.reason.
The delivery headers look like this:
X-Banata-Signature: t=1775038266975,v1=256f786610aa46db35456004b19a85440bb21ae0a131d73b6025013d6f0fc3cc
X-Banata-Timestamp: 1775038266975
X-Banata-Delivery-Attempt: 1
X-Banata-Event-Id: 69fea4ee-f8c5-460d-9b87-fcea6d0bd5ab
X-Banata-Event: sandbox.handoff.requestedTask completion event example
This is the most common async callback:
{
"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"
}
}
}This is why metadata is useful:
- you send it with
promptAsync() - you receive it back in the webhook
- your app can map the completion to the correct user, job, or record
Webhooks vs direct reads
Use webhooks when:
- the workflow is asynchronous
- your app should react later
Use direct reads when:
- you are debugging
- you are building an admin view
- you want a quick manual status check