Skip to main content
Set up webhooks at cloud.browser-use.com/settings?tab=webhooks.

Events

EventWhen
agent.task.status_updateTask status changes (started, finished, or stopped)
testWebhook test ping

Payload

{  "type": "agent.task.status_update",  "timestamp": "2025-01-15T10:30:00Z",  "payload": {  "task_id": "task_abc123",  "session_id": "session_xyz",  "status": "finished",  "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");  }   const event = JSON.parse(body);   if (event.type === "agent.task.status_update") {  const { task_id, status, session_id } = event.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")   event = await request.json()   if event["type"] == "agent.task.status_update":  task_id = event["payload"]["task_id"]  status = event["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.