BridgPay Docs

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

EventStatus codeDescription
Initiated3Payout request created and queued.
Successful11Funds transferred.
Failed12Bank rejected or reversed.

Webhook headers

HeaderDescription
x-webhook-timestampUnix epoch milliseconds when the webhook was sent.
x-webhook-signatureHMAC-SHA256 hex signature for verification.
x-webhook-algAlgorithm 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.

On this page