> ## Documentation Index
> Fetch the complete documentation index at: https://browseruse-0aece648-magnus-streaming-api-link.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when tasks complete. Configure webhook endpoints for async task monitoring.

Set up webhooks at [cloud.browser-use.com/settings?tab=webhooks](https://cloud.browser-use.com/settings?tab=webhooks).

## Events

| Event                      | When                                                      |
| -------------------------- | --------------------------------------------------------- |
| `agent.task.status_update` | Task status changes (`started`, `finished`, or `stopped`) |
| `test`                     | Webhook test ping                                         |

## Payload

```json theme={null}
{
  "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.

<CodeGroup>
  ```python Python theme={null}
  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)
  ```

  ```typescript TypeScript theme={null}
  import { createHmac, timingSafeEqual } from "crypto";

  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;
  }

  function verifyWebhook(body: string, signature: string, timestamp: string, secret: string): boolean {
    // Reject requests older than 5 minutes
    if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;
    const payload = JSON.parse(body);
    const message = `${timestamp}.${JSON.stringify(sortKeys(payload))}`;
    const expected = createHmac("sha256", secret).update(message).digest("hex");
    return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
  }
  ```
</CodeGroup>

## Example: Express webhook handler

```typescript theme={null}
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

```python theme={null}
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"}
```

<Tip>
  For local development, use a tunneling tool like [ngrok](https://ngrok.com) to expose your local server: `ngrok http 3000`. Then set the ngrok URL as your webhook endpoint in the dashboard.
</Tip>
