The verdict in three sentences
A reliable payment webhook rests on three pillars: verify the signature (HMAC or provider hash), deduplicate by reference with an idempotency key, and replay events through a retry queue with exponential backoff. Without these three guardrails, you confirm phantom orders or charge a customer twice. In 2026, every serious provider — Paystack, Flutterwave, M-Pesa, MTN MoMo, Wave — signs its webhooks: your job is to never trust an unverified callback.
Why webhooks get lost (and duplicated)
A webhook is a simple HTTP POST from the provider to your server. Anything that breaks an HTTP request breaks a webhook: timeout, in-progress deploy, a 500 error, slow DNS. Providers react by replaying the event — the number-one source of duplicates. Your endpoint must therefore be idempotent: receiving the same event twice must produce only one effect.
| Provider | Automatic retry | Interval between attempts | Signature | Estimated loss 2026 |
|---|---|---|---|---|
| Paystack | Yes, up to 72h | Exponential (min -> h) | x-paystack-signature header (HMAC SHA512) | ~1% |
| Flutterwave | Yes, several tries | Progressive | verif-hash (configured secret) | ~1-2% |
| M-Pesa (Daraja) | Callback + reconciliation | Variable | Confirmation URL + status query | ~2% |
| MTN MoMo | Callback + polling advised | Variable | API key + transaction verify | ~2-3% |
| Flutterwave (KE/GH) | Yes | Progressive | Hash secret | ~1-2% |
| Paystack (GH/ZA) | Yes | Exponential | HMAC SHA512 | ~1% |
Golden rule: never treat a payment as final on the webhook alone. Always reconfirm via the provider's verify/status API before fulfilling.
The three technical guardrails
1. Verify the signature
Each provider signs the raw request body with your secret. You recompute the HMAC server-side and compare. Paystack example: HMAC-SHA512(secret, raw_body) must equal the x-paystack-signature header. If it doesn't match, respond 401 and drop the event. Use the raw body, not re-serialized JSON — a single whitespace difference breaks the signature.
2. Idempotency key and deduplication
Store each transaction reference in a table with a uniqueness constraint. On receipt:
| Step | Action | Result |
|---|---|---|
| Webhook received | Extract reference | Unique transaction key |
| INSERT into DB | UNIQUE constraint on reference | Duplicate rejected at SQL level |
| Already processed? | Return 200, do nothing | No double fulfillment |
| New? | Process then mark processed | Single effect guaranteed |
The database uniqueness constraint is your ultimate safety net: even under heavy concurrency, two identical INSERTs fail and only one passes.
3. Retry queue with exponential backoff
If your processing fails (DB down, third-party service unavailable), don't lose the event: push it onto a queue and replay with growing delays: 1 min, 2 min, 4 min, 8 min... up to 24h. Only respond 200 to the provider once the event is acknowledged in the queue, not when all business logic finishes.
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.
Mini case study
Awa runs a cosmetics store in Dakar, about 900 orders/month at an average of 15,000 FCFA. Before the rebuild, her site confirmed orders directly on the webhook, with no deduplication. Result: roughly 1.5% duplicates, about 13 orders/month fulfilled or counted twice, of which 4 required manual refunds at 15,000 FCFA = 60,000 FCFA/month in hard losses, plus 3 hours of reconciliation.
After adding the uniqueness constraint on reference plus reverification via the status API: duplicates dropped to ~0, saving 3 hours/month. Cost of the fix: half a day of dev. Payback in under a month.
FAQ
Should I fulfill the order directly inside the webhook?
No. The webhook should only record the event and trigger idempotent processing. Fulfill after reverifying status via the provider API, because a webhook may arrive for a payment still pending or ultimately failed.
What if I receive the same webhook three times?
Do nothing beyond the first time. Thanks to the UNIQUE constraint on the reference, the 2nd and 3rd receptions are detected as already processed and you return 200 immediately. That's expected behavior, not a bug.
How do I test the signature without breaking production?
Use the provider's sandbox (test keys) and a local tunnel like ngrok. Send a test event, verify your recomputed HMAC matches, then simulate a wrong secret to confirm you correctly reject with 401.
How many webhooks actually get lost?
2026 order of magnitude: 1 to 3% depending on the provider and your endpoint's uptime. That's why fallback polling (checking pending transactions every few minutes) remains essential for serious e-commerce.
One provider per country or an aggregator?
An aggregator (Paystack, Flutterwave, CinetPay) simplifies multi-operator webhook integration, but each provider has its own signature and retry logic. Standardize your receiving layer to absorb these differences.
Let's talk about your project. We integrate idempotent, tested mobile money webhooks for your store. 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.

