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.