Originally published at parvejshah.com/blog/defensive-webhook-engineering-payment-gateways by Parvej Shah.
There's a class of bug that only appears in production, under real network conditions, with real money. A webhook gets delivered twice. A deploy is mid-rollout when the POST arrives and the gateway retries. The first time you see it, a student has either paid without getting access, or has two enrollments for a course they bought once.
For MathPro Academy, students pay through SSLCommerz — the gateway that fronts bKash, Nagad, and card payments in Bangladesh. Its confirmation model is a webhook POST (the IPN), and the mistake most integrations make is trusting that POST's body as-is.
SSLCommerz Doesn't Sign the Payload — So Don't Verify a Signature That Isn't There
Some payment gateways attach an HMAC signature to their webhook payload, and the correct move there is a timing-safe comparison against your own computed hash. SSLCommerz's IPN doesn't work that way. What it gives you instead is a validation API: given the transaction ID from the webhook, you query SSLCommerz's own server directly and it tells you, authoritatively, whether that transaction is real and in what state.
// Simplified from the real handler: never trust the IPN body directly —
// query SSLCommerz's own validation API for the transaction's true state.
const validationUrl =
`${baseUrl}/validator/api/merchantTransIDvalidationAPI.php` +
`?tran_id=${encodeURIComponent(tranId)}` +
`&store_id=${storeId}&store_passwd=${storePassword}&v=1&format=json`;
const result = await fetchJson(validationUrl);
const transaction = result.element?.[0];
if (transaction?.status !== "VALID" && transaction?.status !== "VALIDATED") {
// Reject — the gateway itself doesn't vouch for this transaction.
return;
}
The trust anchor isn't a shared secret compared byte-for-byte; it's a live round trip to a server SSLCommerz controls. That's a different shape of guarantee than HMAC, but not a weaker one for this use case — an attacker can't spoof a VALIDATED response from SSLCommerz's own infrastructure.
The Amount Has to Match What You Actually Charged
The validation response includes the amount SSLCommerz actually processed. The handler compares that against the price recorded at checkout time — not the price in the IPN body, which a forged or replayed request could carry stale or altered. A mismatch fails the transaction outright, logged for manual reconciliation rather than silently fulfilled.
Fraud Scoring You Don't Have to Build
SSLCommerz's validation response also includes a risk_level field — its own assessment of whether a transaction looks fraudulent. The handler checks it and flags risky transactions rather than treating every VALID status as equally trustworthy. This is fraud detection MathPro didn't have to build from scratch; it's already sitting in a field on a response the handler was querying anyway.
Idempotency Without a Row Lock
The validation response itself distinguishes VALID (first time seen) from VALIDATED (already processed) — so a re-delivered IPN is caught by asking SSLCommerz "have I already told you this succeeded?" rather than by locking a row in MathPro's own database. On the enrollment write itself, a duplicate is handled as an expected, logged outcome — not a failure — since a retried webhook that's already been fulfilled shouldn't error the second time it arrives.
Every attempt — successful, rejected, or errored — is written to a payment audit log with the raw IPN payload, so a support ticket about "I paid but didn't get access" has a full trail to check rather than a guess.
What We Learned
The instinct to reach for HMAC and crypto.timingSafeEqual is a reasonable one — it's the right tool for gateways that actually sign their payloads. But defensive webhook engineering isn't about applying the strongest-sounding cryptographic primitive available; it's about matching the guarantee you build to the guarantee the gateway actually offers. SSLCommerz offers a query-back validation API and a risk score, not a signature — and building around what's actually there turned out to need less custom cryptography, not more.
Parvej Shah is a Lead Full-Stack Web Developer & Platform Architect based in Dhaka, Bangladesh. Explore full architecture case studies and production code at parvejshah.com.
Top comments (0)