Webhooks
Bridge calls you. Signed events for everything that happens, delivered to a URL you own, retried for about 21 hours, and replayable after that.
A webhook is Bridge making an HTTP request to you. When something you asked to hear about happens (a transfer completes, a virtual account is credited, a customer’s KYC tier moves), Bridge sends a signed JSON document to a URL you registered, and keeps trying until you acknowledge it.
Polling is worse in every direction: slower when you need speed, more expensive when nothing is happening, and blind to anything with no resource to poll. A payment that arrives into a virtual account at 03:00 is the clearest case.
How it works
- You register an endpoint. A URL you control, plus the event types you want. Bridge issues a signing secret and shows it to you once.
- Something happens. Bridge records the event once, with the payload rendered at that moment.
- Bridge works out who wants it. One delivery per subscribed endpoint, byte-identical across endpoints.
- Bridge signs and sends. A POST to your URL with a
Bridge-Signatureheader computed over the exact bytes of the body. - You verify, dedupe and acknowledge. Any 2xx within ten seconds ends it. Do the work afterwards.
- If you didn’t answer, Bridge tries again. Up to eight attempts across about 21 hours, then the delivery is
DEADand waits for you to replay it.
The four things that will bite you
- Verify before you parse. Verify the signature against the raw body bytes. Parsing the JSON and re-serialising it changes the bytes, and the signature never matches. In Express that means
express.raw(), notexpress.json(). idis the idempotency key. Delivery is at least once, which means occasionally twice. Store the envelope’sid; if you’ve seen it, return 200 and do nothing.- Acknowledge first, work after. You have ten seconds. Return 200 once the signature checks out and the event is recorded, then do the work on a queue.
- Subscribe to the failures too. Every flow that can fail has its own failure event. A system that only hears about success shows a payout as pending forever.
Routing on type
switch (event.type) {
case "transfer.completed": return onTransfer(event.data);
case "withdrawal.completed": return onPayoutSettled(event.data);
case "withdrawal.failed": return onPayoutFailed(event.data); // subscribe to both
case "virtual_account.credited": return onIncomingPayment(event.data);
default: return; // 200, and ignore
}
An event you don’t recognise is a 200 and nothing else. Never a 4xx: a 4xx is retried eight times and then dead-lettered. This matters most if you subscribe with a pattern like escrow.*, because a new event type starts arriving the day it ships.
The main events are in the event catalogue. The API serves the full list from GET /webhook/event-type, so your tooling never drifts from what the platform sends.
Test and live are separate. An endpoint registered with
X-Environment: testreceives test events only.http://URLs are allowed in test, and only in test.
The integration checklist
- The signing secret is in a secret store, not in the repository
- The receiver reads the raw body, not a parsed object
- The receiver checks every
v1=value in the signature header - The comparison is constant-time
- There is a timestamp tolerance, and the server clock is on NTP
- Event ids are stored, and a repeated id returns 200 without re-processing
- The handler acknowledges within ten seconds and works asynchronously
- Unrecognised event types return 200
- You’re subscribed to the failure events, not only the success ones
- https, a valid certificate and a publicly resolvable host
- A test delivery has arrived and verified end to end
- Someone is alerted when Bridge auto-pauses the endpoint