Gareth Wilson Gareth Wilson

Guide to Browserbase Webhooks: Features and Best Practices

Published


Browserbase runs headless browsers for AI agents and automation, and Browserbase Functions let you deploy code that runs against those browsers. Its webhooks report the lifecycle of that code: an invocation is queued, starts, completes or fails, and a build of your Function starts, completes or fails. Instead of holding a request open or polling until an invocation finishes, you let Browserbase tell you.

This guide covers how Browserbase webhooks are registered and signed, the events they carry, how retries, ordering and secret rotation work, and the best practices for running a Browserbase receiver in production.

What are Browserbase webhooks?

A Browserbase webhook is an HTTPS endpoint registered against a project, subscribed to one or more event types. The API key you use decides which project owns it. You can manage webhooks in Settings in the Browserbase dashboard or with the SDK, and both act on the same webhooks.

Deliveries follow the Standard Webhooks specification. Every POST carries webhook-id, webhook-timestamp and webhook-signature headers, and the signature is an HMAC-SHA256 over the ID, the timestamp and the raw body, keyed with your webhook's whsec_ secret. Browserbase's advice is to verify with a Standard Webhooks library rather than writing the HMAC yourself, and any library that implements the specification works.

Browserbase webhook features

FeatureDetails
ConfigurationSettings in the dashboard, or the SDK and API (bb.webhooks.create)
ScopeOne project. Each endpoint URL can be registered once per project
Subscription modelA list of event types. Unknown types are rejected at registration
Signature headerswebhook-id, webhook-timestamp, webhook-signature
SchemeStandard Webhooks: HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw body}, sent as v1,<base64>
SecretOne per webhook, prefixed whsec_, returned only at creation and on rotation
RotationThe previous secret keeps verifying for 24 hours unless you pass revokeImmediately
Replay windowNone stated by Browserbase
Response windowAny 2xx within 15 seconds. The body is ignored
RetriesImmediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours
Auto-disableAfter five days of continuous failures
OrderingNot guaranteed
Source IPsNot published

Common events

All seven event types come from Functions. An invocation is one run of a Function, and a build is a publish of your Function code.

EventFires when
functions.invocations.pendingBrowserbase accepts and queues an invocation
functions.invocations.runningAn invocation starts executing
functions.invocations.completedAn invocation finishes successfully
functions.invocations.failedAn invocation fails
functions.builds.runningA build starts
functions.builds.completedA build finishes successfully
functions.builds.failedA build fails

Every body is the same envelope:

{
  "id": "85e6ff2d-91de-468a-867c-e0d9e4587063",
  "type": "functions.invocations.completed",
  "resourceId": "a3f1c2d4-7b90-4e51-9c8a-2f6d1e0b4c73",
  "data": {}
}

id identifies the event and stays the same across retries, type is the event type, resourceId is the invocation or build the event concerns, and data holds the type-specific fields. Browserbase doesn't document what data contains for each type, and says the envelope keys are additive: it "may add keys but will not rename or remove them".

Setting up Browserbase webhooks

Create a webhook with the SDK and store the secret straight away, because no endpoint returns it later:

import { Browserbase } from "@browserbasehq/sdk";

const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });

const webhook = await bb.webhooks.create({
  endpoint: "https://example.com/webhooks/browserbase",
  eventTypes: ["functions.invocations.completed", "functions.invocations.failed"],
});

console.log(webhook.secret); // store as BROWSERBASE_WEBHOOK_SECRET

Creating a webhook in Settings shows the secret once in the same way. The endpoint must be https:// with a real host, and registering the same URL twice in one project returns a conflict.

To change the subscription, call bb.webhooks.update with eventTypes. The list replaces the existing one rather than adding to it, so include every type you want to keep.

Securing Browserbase webhooks

Read the three headers, base64-decode the part of your secret after whsec_, compute an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, base64-encode it, and compare it in constant time with each v1, entry in webhook-signature.

const crypto = require('crypto');
const express = require('express');

function verifyBrowserbaseSignature(rawBody, headers, secret) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatureHeader = headers['webhook-signature'];
  if (!id || !timestamp || !signatureHeader || !secret) return false;

  // Optional: Browserbase states no tolerance of its own. If you want a freshness check, e.g.
  // if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const signedContent = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const expected = crypto.createHmac('sha256', key).update(signedContent).digest();

  // During a rotation the header can carry one entry per active secret.
  return signatureHeader.split(' ').some((entry) => {
    const [version, value] = entry.split(',');
    if (version !== 'v1' || !value) return false;
    const received = Buffer.from(value, 'base64');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/browserbase', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.BROWSERBASE_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyBrowserbaseSignature(req.body, req.headers, secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.id, route on event.type, then process asynchronously.
  res.sendStatus(204);
});

The same check in Python:

import base64
import hashlib
import hmac


def verify_browserbase_signature(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signature_header = headers.get("webhook-signature")
    if not (msg_id and timestamp and signature_header and secret):
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed_content = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, value = entry.partition(",")
        if version == "v1" and hmac.compare_digest(value.encode(), expected):
            return True
    return False

Browserbase's own examples use the standardwebhooks package (npm and PyPI): new Webhook(secret).verify(rawBody, headers) in Node and Webhook(secret).verify(body, headers) in Python. The library also applies a timestamp tolerance, which Browserbase leaves to it rather than defining one.

To check your verifier, use the secret whsec_YnJvd3NlcmJhc2UtdGVzdC1zZWNyZXQta2V5, webhook-id msg_2x2zwLOcWaRTzsLK8KtbQt1FTk9, webhook-timestamp 1761039000 and this exact body:

{"id":"85e6ff2d-91de-468a-867c-e0d9e4587063","type":"functions.invocations.completed","resourceId":"a3f1c2d4-7b90-4e51-9c8a-2f6d1e0b4c73","data":{}}

The webhook-signature should be:

v1,U2mpA1mPoSb5yR/HVW6xvFcOGUp25V2H4M54go7G/SE=

Make Browserbase webhooks production-ready. Hookdeck Event Gateway verifies the Standard Webhooks signature on every Browserbase delivery, answers inside the 15-second window, and durably queues each invocation and build event before your service sees it.

Browserbase webhook limitations and pain points

Events for one invocation arrive out of order

The Problem: A completed event arrives before the running event for the same invocation, and your status tracking moves backwards.

Why It Happens: Browserbase says it "doesn't order events for one resource". Retries make this more likely, because a delayed running can arrive after the invocation has finished.

Workarounds:

  • Treat completed and failed as terminal, and ignore non-terminal events for a resource that has already reached one.
  • Key state on resourceId, not on arrival order.

How Hookdeck Can Help: Hookdeck filters can send terminal and non-terminal events to different destinations, so a late running event doesn't overwrite a final state.

The secret is shown once

The Problem: You lose the signing secret and can't get it back.

Why It Happens: Browserbase returns the secret only from create and rotate. List and get calls never return it.

Workarounds:

  • Store the secret in a secret manager as soon as you create the webhook.
  • If it's lost, rotate the secret to get a new one.

How Hookdeck Can Help: Store the secret once on a Hookdeck source and let Hookdeck verify every delivery, so the secret lives in one place rather than in each service that consumes Browserbase events.

Rotation needs a careful order

The Problem: Deliveries fail verification right after a rotation, or a second rotation fails.

Why It Happens: Rotation issues a new secret and keeps the old one verifying for 24 hours, so signatures for both can be in flight. Only a limited number of rotated secrets can be inside their 24-hour window at once, and rotating again before a window closes fails without changing anything.

Workarounds:

  • Rotate, deploy the new secret alongside the old one, confirm traffic verifies, then drop the old secret, which is the order Browserbase recommends.
  • Accept a request if any v1, entry matches.
  • If a secret leaked, pass revokeImmediately so the old one stops working at once.

How Hookdeck Can Help: Update the secret on your Hookdeck source after rotating. Deliveries signed with both secrets keep verifying with either one until the old secret expires.

Five days of failures switch the endpoint off

The Problem: Events stop arriving and nothing in your logs explains it.

Why It Happens: After eight attempts over the retry schedule, Browserbase marks a message as failed. An endpoint that keeps failing for five days is disabled. Redirects count as failures too.

Workarounds:

  • Register the final URL, not one that redirects.
  • Return a 2xx as soon as the event is stored.
  • Check the delivery history in Settings when events stop.

How Hookdeck Can Help: Hookdeck accepts each delivery immediately and stores it, so your downtime doesn't use up Browserbase's retries, and you can replay the stored events once your service recovers.

Best practices

Verify against the raw body

Browserbase warns that "any reserialization changes the bytes and breaks the signature". Use express.raw(), await request.text() in a Next.js route handler, or await request.body() in FastAPI.

Deduplicate on the envelope id

Browserbase retries when it doesn't get a 2xx in 15 seconds, including when your response was simply late. Store id and skip events you've already processed. See our guide to webhook idempotency.

Parse defensively

Ignore envelope keys you don't recognise, and don't assume a shape for data until you've captured real deliveries for each type you handle.

Acknowledge first, process after

The 15-second budget covers your whole response. Store the event, return 2xx, and do the work afterwards. See why to process webhooks asynchronously.

Subscribe to what you use

Updating eventTypes replaces the list, so keep the full list in your deployment code and send it every time.

Conclusion

Browserbase signs every webhook with Standard Webhooks: an HMAC-SHA256 of webhook-id, webhook-timestamp and the raw body, keyed with your base64-decoded whsec_ secret. Store the secret when you create the webhook, verify against the raw bytes, deduplicate on the envelope id, treat terminal states as final because events arrive unordered, and rotate secrets in Browserbase's recommended order.

Hookdeck Event Gateway verifies each Browserbase delivery, stores it, and retries into your service on your schedule.

Get started with Hookdeck for free and handle Browserbase webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.