Websites11 min read

Idempotent Payment Webhooks: Avoiding Double Charges in 2026

Mohamed Bah·Fondateur, Kolonell
August 14, 2026
Share:
Idempotent Payment Webhooks: Avoiding Double Charges in 2026

Idempotent Payment Webhooks: Avoiding Double Charges in 2026

Websites

The verdict in three sentences

A webhook is never sent just once: Paystack, Flutterwave or M-Pesa can retry the same event up to 5 times over 24 hours. Without an idempotency key at the database level, your system fulfills the order twice or refunds twice, and every duplicate costs a lost product margin. The fix is three bricks: a unique key per event, a deduplication table, and an atomic upsert that makes the operation replayable with no side effect.

Why duplicates happen

A webhook is an HTTP call. If your server responds slowly, crashes, or returns a network error after processing the event, the operator considers the delivery failed and retries. So you have already fulfilled, but you receive the same event a second time.

Operator2026 retry countWindowBackoff
PaystackUp to 524 hExponential
FlutterwaveUp to 524 hExponential
M-PesaVariableConfig dependentConfig dependent
WaveUp to 524 hExponential

These figures are 2026 orders of magnitude. The common point: all retry. Your code must assume each event will arrive several times, and behave as if it were the first.

Event, risk and safeguard

Here is the matrix to burn into your architecture. For each event type, a duplicate risk and the safeguard that cancels it.

EventDuplicate riskSafeguard
payment.successDouble fulfillmentUnique key + upsert
payment.successDouble email/SMSnotified flag in DB
refund.completedDouble refundLocked refunded status
payout.sentDouble vendor payoutLedger with transaction key
order.paidDouble stock decrementAtomic transaction
subscription.renewedDouble billingPeriod key + upsert

The universal pattern: each webhook carries a unique identifier (event id or transaction reference). You insert it into a processed_events table with a uniqueness constraint. SQL pseudo-code: INSERT INTO processed_events (event_id, processed_at) VALUES ($1, now()) ON CONFLICT (event_id) DO NOTHING RETURNING event_id;. If the insert returns a row, it is the first time: you process. If it returns nothing (conflict), the event is already handled: you respond 200 and do nothing. The atomicity of the upsert even prevents races between two simultaneous callbacks.

Mini case study

Amara, who runs an artisan marketplace in Kumasi, paid vendors automatically on receiving the payout webhook. One Friday, a latency spike made Flutterwave retry 3 times on the same orders. Without an idempotency key, her system paid 3 times to 14 vendors. On an average basket of 40,000 GHS-equivalent and a 10 % platform commission, each order pays 36,000 to the vendor. The 28 excess payouts (2 duplicates x 14 vendors) meant money wrongly sent out, to be clawed back one by one. After adding a ledger with a unique transaction key and an atomic upsert, duplicates dropped to zero. The fix took half a day.

Need a professional website?

Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.

FAQ

How many times can a webhook be resent in 2026?

Up to 5 times over 24 hours with Paystack, Flutterwave and Wave, using exponential backoff. Your code must treat every event as if it could arrive multiple times.

Which idempotency key should I use?

The operator's unique event identifier or transaction reference. Store it with a uniqueness constraint in the database to block any reprocessing.

Does a duplicate really cost that much?

Yes. A double fulfillment loses a product margin; a double refund or double payout sends out real money. In the example above, a single incident wrongly paid out to 14 vendors three times.

Should I respond 200 even for a duplicate event?

Yes. Respond 200 to tell the operator you received it, otherwise it retries again. Internally, you simply do not replay the processing.

Is an upsert enough against two simultaneous callbacks?

Yes, if the upsert is atomic with a uniqueness constraint (ON CONFLICT DO NOTHING). The database arbitrates the race: one insert wins, the other is cleanly ignored.

Let's talk about your project. We make your payment webhooks idempotent and protect your till from double charges. WhatsApp +221 77 596 93 33.

Tags:#webhooks#idempotence#paiement#double encaissement#wave#paystack#flutterwave#architecture
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.