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.
The envelope
Section titled “The envelope”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"
}
}
| Field | Type | Description |
|---|---|---|
| id | string | Unique envelope ID (evt_ prefix). Use it as your idempotency key: retries reuse the same ID, replays get a fresh one |
| type | string | The event type (see Event types) |
| created | number | Unix timestamp (seconds) when the envelope was built |
| app_id | string | Your app’s ID |
| workspace_id | string? | The workspace the event concerns, when applicable |
| data | object | Event-specific payload |
Each request also carries these headers:
| Header | Description |
|---|---|
| Bridge-Signature | HMAC signature, see below |
| Bridge-Event-Id | Same as the envelope id |
| Bridge-Event-Type | Same as the envelope type |
| Content-Type | application/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.
Verifying the signature
Section titled “Verifying the signature”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.
Retries and failure handling
Section titled “Retries and failure handling”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 attempt | Next retry in |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
| 6 | 24 hours |
| 7 | none: 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
idand usecreatedto order.
Event types
Section titled “Event types”The canonical catalogue is served by GET /webhooks/event-types. Current types:
| Type | Category | Fired when |
|---|---|---|
| tenant.created | tenant | A new workspace was created in this app |
| tenant.updated | tenant | Workspace fields (name, plan, metadata) were updated |
| tenant.deleted | tenant | A workspace was deleted |
| tenant_user.created | tenant_user | A user joined a workspace (invite accepted or signup) |
| tenant_user.updated | tenant_user | A workspace user was updated (role change, profile, etc.) |
| tenant_user.deleted | tenant_user | A user left a workspace or was removed by an admin |
| subscription.created | billing | A new subscription was created for the workspace |
| subscription.plan_changed | billing | The workspace switched plans (upgrade or downgrade) |
| subscription.trial_started | billing | The workspace started a trial on a paid plan |
| subscription.trial_ended | billing | The trial period elapsed without conversion |
| subscription.past_due | billing | A renewal payment failed and the subscription is past due |
| subscription.reactivated | billing | A cancel-at-period-end subscription was reactivated |
| subscription.canceled | billing | The subscription was canceled; endsAt in data is when access ends |
| payment.succeeded | billing | A renewal or one-off invoice was paid successfully |
| payment.failed | billing | A renewal payment failed; pastDueReason drives messaging |
| quota.exhausted | billing | A metered quota crossed 100% of the plan limit |
| entitlements.changed | billing | The workspace entitlement set changed |
| usage.drift_resolved | billing | Reconciliation detected Bridge/Stripe usage drift and auto-healed it |
| usage.drift_unrecoverable | billing | Reconciliation 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.
Management endpoints
Section titled “Management endpoints”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
appIdpath parameters must match the app in the caller’s context; a mismatch returnsHTTP 400.
List event types
Section titled “List event types”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.
HTTP Request
Section titled “HTTP Request”GET https://api.thebridge.dev/webhooks/event-types
Response HTTP 200
Section titled “Response HTTP 200”{ "events": [...] } where each entry is:
| Field | Type | Description |
|---|---|---|
| type | string | The event type, e.g. subscription.canceled |
| category | string | tenant, tenant_user, or billing |
| description | string | Human-readable description |
| samplePayload | object | Illustrative 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"
}
}
]
}
List recent deliveries
Section titled “List recent deliveries”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.
HTTP Request
Section titled “HTTP Request”GET https://api.thebridge.dev/webhooks/deliveries/APP_ID
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
APP_ID | string | Required | Your application ID. Must match the app in the caller's context |
Response HTTP 200
Section titled “Response HTTP 200”An array of delivery records:
| Field | Type | Description |
|---|---|---|
| id | string | The delivery record’s ID (use it to replay) |
| eventId | string | The envelope id |
| eventType | string | The envelope type |
| targetUrl | string | URL snapshotted at dispatch time |
| status | string | pending, delivered, failed, or dlq |
| attempt | number | Current attempt number (1 to 7) |
| responseCode | number? | HTTP status from the last attempt, or null |
| lastError | string? | Error from the last failed attempt, or null |
| nextRetryAt | string? | When the next attempt is due, or null |
| deliveredAt | string? | When the delivery succeeded, or null |
| createdAt | string | When the delivery was enqueued |
| payload | object | The 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" }
}
}
]
Replay a delivery
Section titled “Replay a delivery”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.
HTTP Request
Section titled “HTTP Request”POST https://api.thebridge.dev/webhooks/deliveries/DELIVERY_ID/replay
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
DELIVERY_ID | string | Required | The delivery record's id from the deliveries list. Must belong to the caller's app |
Response HTTP 201
Section titled “Response HTTP 201”| Field | Type | Description |
|---|---|---|
| ok | boolean | Always true |
| deliveryId | string | ID 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"
}
Rotate the signing secret
Section titled “Rotate the signing secret”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.
HTTP Request
Section titled “HTTP Request”POST https://api.thebridge.dev/webhooks/secrets/rotate/APP_ID
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
APP_ID | string | Required | Your application ID. Must match the app in the caller's context |
Response HTTP 201
Section titled “Response HTTP 201”| Field | Type | Description |
|---|---|---|
| secret | string | The new signing secret (whsec_...) |
| rotatedAt | string | ISO-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"
}