Documentation Index
Fetch the complete documentation index at: https://browseruse-0aece648-bux-docs.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Set up webhooks at cloud.browser-use.com/settings?tab=webhooks.
Events
| Event | When |
|---|
agent.task.status_update | Task status changes (running, idle, or stopped) |
test | Webhook test ping |
Payload
{
"type": "agent.task.status_update",
"timestamp": "2025-01-15T10:30:00Z",
"payload": {
"task_id": "task_abc123",
"session_id": "session_xyz",
"status": "idle",
"metadata": {}
}
}
Signature verification
Every webhook request includes two headers:
X-Browser-Use-Signature — HMAC-SHA256 signature of the payload
X-Browser-Use-Timestamp — Unix timestamp (seconds) when the request was sent
The signature is computed over {timestamp}.{body}, where body is the JSON-serialized payload with keys sorted alphabetically and no extra whitespace. Verify it to ensure the request is authentic and to prevent replay attacks.
import hashlib
import hmac
import json
import time
def verify_webhook(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
# Reject requests older than 5 minutes
try:
ts = int(timestamp)
except (ValueError, TypeError):
return False
if abs(time.time() - ts) > 300:
return False
payload = json.loads(body)
message = f"{timestamp}.{json.dumps(payload, separators=(',', ':'), sort_keys=True)}"
expected = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Example: Express webhook handler
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";
const app = express();
app.use(express.raw({ type: "application/json" }));
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
function sortKeys(obj: unknown): unknown {
if (Array.isArray(obj)) return obj.map(sortKeys);
if (obj !== null && typeof obj === "object") {
return Object.keys(obj as object)
.sort()
.reduce((acc, key) => {
(acc as Record<string, unknown>)[key] = sortKeys((obj as Record<string, unknown>)[key]);
return acc;
}, {} as Record<string, unknown>);
}
return obj;
}
app.post("/webhook", (req, res) => {
const signature = req.headers["x-browser-use-signature"] as string;
const timestamp = req.headers["x-browser-use-timestamp"] as string;
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(401).send("Request too old");
}
const body = req.body.toString();
const payload = JSON.parse(body);
const message = `${timestamp}.${JSON.stringify(sortKeys(payload))}`;
const expected = createHmac("sha256", WEBHOOK_SECRET).update(message).digest("hex");
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).send("Invalid signature");
}
if (payload.type === "agent.task.status_update") {
const { task_id, status, session_id } = payload.payload;
console.log(`Task ${task_id} is now ${status}`);
}
res.status(200).send("OK");
});
app.listen(3000);
Example: FastAPI webhook handler
from fastapi import FastAPI, Request, HTTPException
import hashlib
import hmac
import json
import os
import time
app = FastAPI()
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]
@app.post("/webhook")
async def handle_webhook(request: Request):
body = await request.body()
signature = request.headers.get("x-browser-use-signature", "")
timestamp = request.headers.get("x-browser-use-timestamp", "")
# Reject requests older than 5 minutes
try:
ts = int(timestamp)
except (ValueError, TypeError):
raise HTTPException(status_code=401, detail="Invalid timestamp")
if abs(time.time() - ts) > 300:
raise HTTPException(status_code=401, detail="Request too old")
payload = json.loads(body)
message = f"{timestamp}.{json.dumps(payload, separators=(',', ':'), sort_keys=True)}"
expected = hmac.new(WEBHOOK_SECRET.encode(), message.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise HTTPException(status_code=401, detail="Invalid signature")
if payload["type"] == "agent.task.status_update":
task_id = payload["payload"]["task_id"]
status = payload["payload"]["status"]
print(f"Task {task_id} is now {status}")
return {"status": "ok"}
For local development, use a tunneling tool like ngrok to expose your local server: ngrok http 3000. Then set the ngrok URL as your webhook endpoint in the dashboard.