Skip to content

Webhooks

Bridge can POST an event to your server whenever something happens in your app: a workspace is created (a workspace, also called a tenant, is one customer account in your app), a user joins, a subscription changes, a payment fails, a quota runs out. You configure the receiving endpoint in Control Center (Bridge’s admin dashboard) on your app’s profile: the webhookUrl, an on/off switch, and an event filter that whitelists which event types you want delivered. Only event types you have subscribed to in the filter are sent.

This page covers the delivery model (envelope, signature, retries) and the REST endpoints for managing the webhook substrate: listing event types, inspecting and replaying deliveries, and rotating the signing secret.

Every webhook is a POST to your webhookUrl with a JSON body in this shape:

{
  "id": "evt_9f8e7d6c-5b4a-3f2e-1d0c-9b8a7f6e5d4c",
  "type": "subscription.plan_changed",
  "created": 1751875200,
  "app_id": "624c14cc0c01e70033356280",
  "workspace_id": "624c14cc0c01e70033356285",
  "data": {
    "workspaceId": "ws_2abc123",
    "fromPlan": "free",
    "toPlan": "pro"
  }
}
FieldTypeDescription
idstringUnique envelope ID (evt_ prefix). Use it as your idempotency key: retries reuse the same ID, replays get a fresh one
typestringThe event type (see Event types)
creatednumberUnix timestamp (seconds) when the envelope was built
app_idstringYour app’s ID
workspace_idstring?The workspace the event concerns, when applicable
dataobjectEvent-specific payload

Each request also carries these headers:

HeaderDescription
Bridge-SignatureHMAC signature, see below
Bridge-Event-IdSame as the envelope id
Bridge-Event-TypeSame as the envelope type
Content-Typeapplication/json

Legacy envelope retired. The old { "events": [{ "type": "TENANT_CREATED", ... }] } batch envelope and its SCREAMING_CASE event names are no longer sent. Consumers must handle the envelope above with the dot-separated event types listed on this page.

The Bridge-Signature header is Stripe-style:

Bridge-Signature: t=1751875200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 is HMAC-SHA256(secret, "<t>.<raw body>") in hex, where secret is your app’s webhook signing secret (a whsec_... value shown in Control Center and returned by rotate secret). Signing the timestamp together with the body means a captured request cannot be replayed later: reject signatures whose t is more than 5 minutes from your current time, and compare digests with a constant-time comparison.

import { createHmac, timingSafeEqual } from 'crypto';

function verify(secret: string, header: string, rawBody: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
  const ageMs = Math.abs(Date.now() - Number(parts.t) * 1000);
  if (ageMs > 5 * 60 * 1000) return false;

  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(parts.v1, 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Compute the HMAC over the raw request body bytes, before any JSON parsing or re-serialization.

Respond with any 2xx status within 10 seconds to acknowledge a delivery. Anything else (non-2xx, timeout, connection error) counts as a failed attempt, and Bridge retries with backoff:

After attemptNext retry in
11 minute
25 minutes
330 minutes
42 hours
512 hours
624 hours
7none: the delivery is parked as dlq

A delivery makes at most 7 attempts, then lands in the dead-letter queue (status: "dlq") where you can replay it manually. Pending deliveries are swept about once a minute, and each delivery’s target URL is snapshotted at dispatch time, so changing your webhookUrl does not retarget in-flight retries.

If 50 deliveries in a row exhaust their retries, Bridge automatically disables the webhook on your app and raises an operator alert. Re-enable it in Control Center once your endpoint is healthy, then replay what you missed.

Deliveries are retried independently, so events can arrive out of order and (on replay or edge cases) more than once. Deduplicate on the envelope id and use created to order.

The canonical catalogue is served by GET /webhooks/event-types. Current types:

TypeCategoryFired when
tenant.createdtenantA new workspace was created in this app
tenant.updatedtenantWorkspace fields (name, plan, metadata) were updated
tenant.deletedtenantA workspace was deleted
tenant_user.createdtenant_userA user joined a workspace (invite accepted or signup)
tenant_user.updatedtenant_userA workspace user was updated (role change, profile, etc.)
tenant_user.deletedtenant_userA user left a workspace or was removed by an admin
subscription.createdbillingA new subscription was created for the workspace
subscription.plan_changedbillingThe workspace switched plans (upgrade or downgrade)
subscription.trial_startedbillingThe workspace started a trial on a paid plan
subscription.trial_endedbillingThe trial period elapsed without conversion
subscription.past_duebillingA renewal payment failed and the subscription is past due
subscription.reactivatedbillingA cancel-at-period-end subscription was reactivated
subscription.canceledbillingThe subscription was canceled; endsAt in data is when access ends
payment.succeededbillingA renewal or one-off invoice was paid successfully
payment.failedbillingA renewal payment failed; pastDueReason drives messaging
quota.exhaustedbillingA metered quota crossed 100% of the plan limit
entitlements.changedbillingThe workspace entitlement set changed
usage.drift_resolvedbillingReconciliation detected Bridge/Stripe usage drift and auto-healed it
usage.drift_unrecoverablebillingReconciliation exhausted retries healing drift; investigate manually

Two high-frequency SDK events, quota.updated and usage.recorded, are never delivered over webhooks. They exist only on the realtime SDK channel, so they cannot flood your endpoint.


Authentication. These endpoints act on your app’s own webhook configuration and authenticate the same way as Usage & Quotas: a signed-in user’s access token plus your app ID:

Authorization: Bearer <USER_ACCESS_TOKEN>
x-app-id: <YOUR_APP_ID>

The appId path parameters must match the app in the caller’s context; a mismatch returns HTTP 400.

The full catalogue of event types Bridge can deliver, with a sample data payload for each. Control Center renders its subscription checklist from this list; use it to build your own event-filter UI or to keep consumer code in sync.

GET https://api.thebridge.dev/webhooks/event-types

{ "events": [...] } where each entry is:

FieldTypeDescription
typestringThe event type, e.g. subscription.canceled
categorystringtenant, tenant_user, or billing
descriptionstringHuman-readable description
samplePayloadobjectIllustrative example of the envelope’s data field

Request example

curl --request GET 'https://api.thebridge.dev/webhooks/event-types' \
--header 'Authorization: Bearer USER_ACCESS_TOKEN' \
--header 'x-app-id: YOUR_APP_ID'

Response example (truncated):

{
  "events": [
    {
      "type": "tenant.created",
      "category": "tenant",
      "description": "A new workspace was created in this app.",
      "samplePayload": { "id": "ws_2abc123", "name": "Acme Inc.", "plan": "pro" }
    },
    {
      "type": "payment.failed",
      "category": "billing",
      "description": "A renewal payment failed. Use `pastDueReason` to drive customer-facing messaging.",
      "samplePayload": {
        "workspaceId": "ws_2abc123",
        "pastDueReason": "card_declined",
        "cardLast4": "4242"
      }
    }
  ]
}

The 20 most recent delivery records for your app, newest first, including the full payload that was (or will be) POSTed. Use this to debug a misbehaving receiver or to find dlq rows to replay.

GET https://api.thebridge.dev/webhooks/deliveries/APP_ID

Path Parameters

ParameterTypeRequiredDescription
APP_IDstringRequiredYour application ID. Must match the app in the caller's context

An array of delivery records:

FieldTypeDescription
idstringThe delivery record’s ID (use it to replay)
eventIdstringThe envelope id
eventTypestringThe envelope type
targetUrlstringURL snapshotted at dispatch time
statusstringpending, delivered, failed, or dlq
attemptnumberCurrent attempt number (1 to 7)
responseCodenumber?HTTP status from the last attempt, or null
lastErrorstring?Error from the last failed attempt, or null
nextRetryAtstring?When the next attempt is due, or null
deliveredAtstring?When the delivery succeeded, or null
createdAtstringWhen the delivery was enqueued
payloadobjectThe full envelope

Request example

curl --request GET 'https://api.thebridge.dev/webhooks/deliveries/YOUR_APP_ID' \
--header 'Authorization: Bearer USER_ACCESS_TOKEN' \
--header 'x-app-id: YOUR_APP_ID'

Response example:

[
  {
    "id": "665f2c9e6d1b4f4e9d1a6a30",
    "eventId": "evt_9f8e7d6c-5b4a-3f2e-1d0c-9b8a7f6e5d4c",
    "eventType": "subscription.plan_changed",
    "targetUrl": "https://your-app.example.com/bridge/webhook",
    "status": "delivered",
    "attempt": 1,
    "responseCode": 200,
    "lastError": null,
    "nextRetryAt": null,
    "deliveredAt": "2026-07-07T08:00:03.000Z",
    "createdAt": "2026-07-07T08:00:00.000Z",
    "payload": {
      "id": "evt_9f8e7d6c-5b4a-3f2e-1d0c-9b8a7f6e5d4c",
      "type": "subscription.plan_changed",
      "created": 1751875200,
      "app_id": "624c14cc0c01e70033356280",
      "workspace_id": "624c14cc0c01e70033356285",
      "data": { "workspaceId": "ws_2abc123", "fromPlan": "free", "toPlan": "pro" }
    }
  }
]

Re-issue a delivery from scratch. The original record is preserved for audit; the replay creates a new delivery starting at attempt 1, with a fresh envelope id (prefixed evt_replay_) and a fresh created timestamp, sent to your current webhook configuration. Typically used on dlq rows after fixing your receiver.

POST https://api.thebridge.dev/webhooks/deliveries/DELIVERY_ID/replay

Path Parameters

ParameterTypeRequiredDescription
DELIVERY_IDstringRequiredThe delivery record's id from the deliveries list. Must belong to the caller's app
FieldTypeDescription
okbooleanAlways true
deliveryIdstringID of the newly created delivery record

Returns HTTP 404 when the delivery does not exist.

Request example

curl --request POST 'https://api.thebridge.dev/webhooks/deliveries/665f2c9e6d1b4f4e9d1a6a30/replay' \
--header 'Authorization: Bearer USER_ACCESS_TOKEN' \
--header 'x-app-id: YOUR_APP_ID'

Response example:

{
  "ok": true,
  "deliveryId": "665f2c9e6d1b4f4e9d1a6a45"
}

Generate a new webhook signing secret for your app. The previous secret remains valid for a 5-minute grace window, so you can deploy the new secret to your receiver without dropping in-flight deliveries. Store the returned secret immediately; it is what you verify Bridge-Signature against.

POST https://api.thebridge.dev/webhooks/secrets/rotate/APP_ID

Path Parameters

ParameterTypeRequiredDescription
APP_IDstringRequiredYour application ID. Must match the app in the caller's context
FieldTypeDescription
secretstringThe new signing secret (whsec_...)
rotatedAtstringISO-8601 timestamp of the rotation

Request example

curl --request POST 'https://api.thebridge.dev/webhooks/secrets/rotate/YOUR_APP_ID' \
--header 'Authorization: Bearer USER_ACCESS_TOKEN' \
--header 'x-app-id: YOUR_APP_ID'

Response example:

{
  "secret": "whsec_9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d",
  "rotatedAt": "2026-07-07T08:12:00.000Z"
}