Websites11 min read

Building Reliable Mobile Money Webhooks: Idempotency and Double Charges (2026)

Mohamed Bah·Fondateur, Kolonell
August 5, 2026
Share:
Building Reliable Mobile Money Webhooks: Idempotency and Double Charges (2026)

Building Reliable Mobile Money Webhooks: Idempotency and Double Charges (2026)

Websites

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.

ProviderAutomatic retryInterval between attemptsSignatureEstimated loss 2026
PaystackYes, up to 72hExponential (min -> h)x-paystack-signature header (HMAC SHA512)~1%
FlutterwaveYes, several triesProgressiveverif-hash (configured secret)~1-2%
M-Pesa (Daraja)Callback + reconciliationVariableConfirmation URL + status query~2%
MTN MoMoCallback + polling advisedVariableAPI key + transaction verify~2-3%
Flutterwave (KE/GH)YesProgressiveHash secret~1-2%
Paystack (GH/ZA)YesExponentialHMAC 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:

StepActionResult
Webhook receivedExtract referenceUnique transaction key
INSERT into DBUNIQUE constraint on referenceDuplicate rejected at SQL level
Already processed?Return 200, do nothingNo double fulfillment
New?Process then mark processedSingle 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.

Tags:#webhook#idempotency#integration#payment#HMAC#Paystack#Flutterwave#developer
Share:

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.