Part 4 · 2 chapters · ~12 min
Rails, Webhooks and Callbacks
How payment rails behave (instant, batch, card networks), callbacks versus polling, verifying and deduplicating webhooks, acknowledging fast and processing later, out-of-order events, confirming state with the provider API, and adapters that hide each rail's quirks.
9
How rails behave
| rail type | examples | behaviour engineers must handle |
|---|---|---|
| instant account-to-account | NIP (Nigeria), Pix, UPI, Faster Payments | seconds, but timeouts and later reversals happen; name enquiry first |
| mobile money | M-Pesa, MTN MoMo | asynchronous callbacks; customer must approve on phone |
| batch / ACH-style | bulk payroll files | submitted today, settled tomorrow; returns days later |
| cards | Visa, Mastercard, Verve via processors | authorise, capture, settle in batches; chargebacks weeks later |
| processors / aggregators | Paystack, Flutterwave, Stripe | webhooks, their own idempotency, settlement to your bank on a schedule |
10
Webhooks, done safely
code
// verify a signed webhook before trusting it (HMAC-SHA512 style, as many processors use)
import { createHmac, timingSafeEqual } from 'node:crypto';
app.post('/webhooks/processor', express.raw({ type: '*/*' }), async (req, res) => {
const sig = Buffer.from(req.get('x-signature') ?? '', 'hex');
const mac = createHmac('sha512', process.env.WEBHOOK_SECRET).update(req.body).digest();
if (sig.length !== mac.length || !timingSafeEqual(sig, mac)) return res.sendStatus(401);
const evt = JSON.parse(req.body.toString('utf8'));
await db.query('INSERT INTO inbound_events (provider, event_id, type, payload) VALUES ($1,$2,$3,$4) ON CONFLICT DO NOTHING',
['processor', evt.id, evt.event, evt]);
res.sendStatus(200); // a worker processes inbound_events
});RECEIVING A PROCESSOR WEBHOOK SAFELY
verify, dedupe, record, acknowledge fast, process later
swipe the figure sideways, or tap expand for full screen
1/5
verify
Check the signature (HMAC over the raw body with the shared secret) and the timestamp to reject forged and replayed webhooks.
HMAC over the raw body + timestamp windowparse JSON after verifying