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.
| Operator | 2026 retry count | Window | Backoff |
|---|---|---|---|
| Paystack | Up to 5 | 24 h | Exponential |
| Flutterwave | Up to 5 | 24 h | Exponential |
| M-Pesa | Variable | Config dependent | Config dependent |
| Wave | Up to 5 | 24 h | Exponential |
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.
| Event | Duplicate risk | Safeguard |
|---|---|---|
| payment.success | Double fulfillment | Unique key + upsert |
| payment.success | Double email/SMS | notified flag in DB |
| refund.completed | Double refund | Locked refunded status |
| payout.sent | Double vendor payout | Ledger with transaction key |
| order.paid | Double stock decrement | Atomic transaction |
| subscription.renewed | Double billing | Period 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.
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.

