Webhooks
Real-time payout status updates delivered to your HTTPS endpoint. HMAC-SHA256 signed; verify before processing.
Webhooks let your application receive payout status changes the moment they happen, without polling.
Configure a webhook URL
Set your webhook URL under Settings → Webhooks in the merchant portal. The signing secret shown there is what you use to verify incoming webhooks.
Webhook payload
{
"payoutWebhookId": "1ee3be28-0330-48eb-b89c-8290413c81f8",
"event": "Successful",
"data": {
"status": 11,
"timestamp": "2026-03-04 17:05:15.000000",
"businessId": "1b313206-049a-11f1-8a8e-0a0c26167b3f",
"responseCode": "100",
"transactionId": "BMPT2026030400007",
"responseMessage": "Operation Success",
"payoutTransactionId": "45e254c0-17ec-11f1-8a8e-0a0c26167b3f"
}
}
Event types
| Event | Status code | Description |
|---|---|---|
Initiated | 3 | Payout request created and queued. |
Successful | 11 | Funds transferred. |
Failed | 12 | Bank rejected or reversed. |
Webhook headers
| Header | Description |
|---|---|
x-webhook-timestamp | Unix epoch milliseconds when the webhook was sent. |
x-webhook-signature | HMAC-SHA256 hex signature for verification. |
x-webhook-alg | Algorithm name (sha256). Required — a missing alg header is a downgrade signal and must be rejected. |
Signature verification
Canonical string
TIMESTAMP|RAW_REQUEST_BODY
Use the exact raw request body received by your server — do not parse or re-serialise it. Any whitespace or key-ordering change invalidates the signature.
Webhooks older than 5 minutes must be rejected to prevent replay attacks. Ensure your server clock is NTP-synced.
TypeScript
import crypto from 'crypto';
export function verifyWebhookSignature({
timestamp,
rawBody,
signature,
webhookSecret,
}: {
timestamp: string;
rawBody: string;
signature: string;
webhookSecret: string;
}): boolean {
const tolerance = 5 * 60 * 1000;
if (Math.abs(Date.now() - Number(timestamp)) > tolerance) {
throw new Error('Webhook timestamp expired');
}
const canonical = `${timestamp}|${rawBody}`;
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(canonical)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex'),
);
}
Receiving webhooks (Express)
import express from 'express';
const app = express();
// Capture raw body BEFORE express.json() parses it.
app.use(
express.json({
verify: (req, _res, buf) => {
(req as any).rawBody = buf.toString();
},
}),
);
app.post('/webhook', (req, res) => {
const timestamp = req.headers['x-webhook-timestamp'] as string;
const signature = req.headers['x-webhook-signature'] as string;
const rawBody = (req as any).rawBody as string;
const ok = verifyWebhookSignature({
timestamp,
rawBody,
signature,
webhookSecret: process.env.BRIDG_WEBHOOK_SECRET!,
});
if (!ok) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Respond immediately, process asynchronously.
res.status(200).json({ received: true });
const { event, data } = req.body;
void processWebhookEvent(event, data);
});
Best practices
- Always verify the signature before doing anything with the payload.
- Respond with 200 immediately. If your endpoint is slow, the webhook is retried with exponential backoff and eventually marked failed. Process the event in a background job.
- Store the webhook secret in a secrets manager — environment variable at minimum. Never hardcode it.
- Dedupe by
payoutWebhookId. The same webhook may be delivered more than once (retries, network blips); your handler must be idempotent. - Keep server time NTP-synced. Webhooks older than 5 minutes are rejected.