Webhooks
BD Voucher receives webhooks from payment and voucher providers, and forwards a normalized set of events to your configured receiver URLs.
Payment webhooks
These are the inbound endpoints the gateways call back into BD Voucher.
We always re-verify each one server-side before marking a payment
SUCCEEDED. See the Payments guide for the full flow.
| Source | Path | Verb | Verification used |
|---|---|---|---|
| SSLCommerz | /api/v1/payments/sslcommerz/callback | POST | validate / transactionQueryByTransactionId |
| bKash | /api/v1/payments/bkash/callback | GET/POST | executePayment (transactionStatus === "Completed") |
| PayStation | /api/v1/payments/paystation/callback | GET/POST | /transaction-status (trx_status === "successful") |
| Zendit | /api/v1/zendit/webhook | POST | HMAC signature |
Each of the three payment endpoints is idempotent — a payment that’s
already SUCCEEDED is a no-op. Concurrent callbacks for the same order
are serialized via a Redis lock keyed by order_id so we never
double-fulfill.
Signature verification
All outbound webhooks are signed with HMAC-SHA256:
X-BDV-Signature: sha256=hex(hmac_sha256(secret, rawBody))X-BDV-Timestamp: 1713648392Verify in Node.js:
import crypto from 'node:crypto';
function verify(rawBody, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected), );}Retries
Failed deliveries (non-2xx) are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 30 s |
| 3 | 2 min |
| 4 | 10 min |
| 5 | 1 h |
After five failures the event is moved to the dead-letter queue; operators can replay via the admin API.
Idempotency
Every event carries a stable eventId. Your handler should be idempotent —
store processed eventIds and short-circuit duplicates.