The verdict in three sentences
A payment webhook is not a single guaranteed call: it is retried 3 to 10 times over 24 to 72 hours, out of order, sometimes after your server times out. Without an idempotency key, HMAC signature verification and replay protection, you book double charges and lose orders. The golden rule: verify the signature, dedupe by event id, return 200 fast, process asynchronously, and reconcile with the status API as the source of truth.
Why webhooks arrive twice
Providers treat a webhook as delivered only if you return 200 quickly. The slightest delay triggers a retry. Here are typical 2026 policies (orders of magnitude, verify against each provider's docs).
| Provider | Retry count | Retry window | Expected timeout | Signature |
|---|---|---|---|---|
| Paystack | up to ~5 | ~72 h | < 10 s | x-paystack-signature (HMAC SHA512) |
| Wave | 3 to 10 | 24-48 h | < 10 s | token / signature |
| CinetPay | 3 to 8 | ~48 h | < 15 s | verification token |
| Flutterwave | up to ~5 | ~24 h | < 10 s | verif-hash header |
| Stripe | up to ~15 over 72 h | 72 h | < 10 s | Stripe-Signature |
Bottom line: ALWAYS design your endpoint as if it will receive each event several times. Idempotency is not optional.
The 5 most common bugs
| Bug | Consequence | Fix |
|---|---|---|
| No signature verification | Fake webhook -> fake paid order | Verify HMAC before any processing |
| Heavy synchronous processing | Timeout -> retry -> double charge | Return 200 fast, process in async queue |
| No deduplication | Same event processed twice | Store event id, ignore if already seen |
| Trusting the payload amount | Forged amount accepted | Reconcile via the status API |
| Returning 500 on business error | Endless provider retries | Return 200 + log to reprocess yourself |
These five mistakes account for nearly all the payment incidents we audit at Kolonell.
The robust 6-step pattern
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.
- Verify the signature (HMAC) with the provider secret, before reading anything else. Invalid signature -> 401.
- Extract the event id and check the database whether it was already processed. Already seen -> return 200 and exit.
- Store the raw event (event id, payload, date) in a log table.
- Return 200 immediately, before any heavy business processing.
- Process asynchronously (queue): update the order, send the invoice, notify.
- Reconcile against the provider
getStatusAPI as the source of truth before marking "paid".
Mini case study
Ibrahim sells event tickets online in Abidjan. One Friday night his server is under load: the Wave webhook takes 12 seconds to respond, the timeout is 10 s. Wave retries the event 4 times. Without idempotency, Ibrahim issues 4 tickets for a single 15,000 FCFA payment; the customer is charged only once, but Ibrahim sends 4 valid tickets, 3 of which get used fraudulently: a 45,000 FCFA loss on one order. After the fix (dedupe by event id + immediate 200 + async processing), the same scenario issues a single ticket regardless of the retry count. Over a weekend of 400 tickets, the incident would have cost several hundred thousand FCFA.
FAQ
Why verify the signature if the webhook URL is secret? Because a URL is never truly secret: it leaks in logs, proxies, network captures. The HMAC signature proves the message really comes from the provider and was not tampered with. Without it, anyone knowing the URL can fake a payment.
What does idempotency mean concretely? It means processing the same event once or ten times produces exactly the same result. In practice: store the provider event id and ignore any already-recorded event. An order flips to "paid" only once, even after 10 retries.
Should I trust the amount in the webhook payload? No. The payload may be incomplete or, in a breach, forged. Always reconcile the amount and status via the provider status API before fulfilling the order.
Why return 200 before processing? Because the provider expects a fast acknowledgment (often < 10 s). If your business logic is slow, you exceed the timeout and trigger retries. Acknowledge first, then process in an async queue.
How do I test all this without real payments? Use the provider sandbox and its event replay feature. Send the same event twice to verify deduplication, send an invalid signature to verify the 401 rejection. Kolonell ships every integration with this test battery.
Let's talk about your project. We audit your webhooks or ship an idempotent, signed payment integration, tested in sandbox. WhatsApp +221 77 596 93 33.
Mohamed Bah
Fondateur, Kolonell
Passionate about digital and entrepreneurship in Africa, Mohamed has been helping Sénégalese businesses with their digital transformation since 2020. Founder of Kolonell, he believes every SME deserves a professional and accessible online présence.
