The verdict in three sentences
A payment webhook is never guaranteed unique: the aggregator may resend it 2 to 5 times on a timeout or retry. Without an idempotency key, your system credits the same order several times, ships twice, or skews your accounting. The defense comes down to three reflexes: verify the signature, store the transaction ID as a unique key, and lock the processing.
Why duplicates happen
Aggregators guarantee "at least once" delivery, not "exactly once". If your server responds slowly or returns an error, the aggregator assumes the message failed and replays it. Your code then receives the same payment event twice.
| Resend cause | Relative frequency | Consequence without idempotency |
|---|---|---|
| Your server timeout | Frequent | Webhook replayed 1-4 times |
| Non-200 HTTP response | Frequent | Automatic retry |
| Unstable network | Medium | Duplicated delivery |
| Aggregator maintenance | Rare | Batch replay |
| Application bug (exception) | Variable | Double processing |
The observed order of magnitude: 1 to 3% of transactions can generate an unhandled duplicate if the integration does not protect processing.
The idempotency pattern, concretely
The principle: each webhook carries a unique transaction ID provided by the aggregator. Before processing, you check whether that ID has already been handled. If yes, you respond 200 and do nothing; if no, you process then record the ID.
| Step | Technical action | Effect |
|---|---|---|
| 1. Verify signature | Compare the HMAC with your secret | Rejects fake webhooks |
| 2. Read transaction ID | Extract the unique key from the payload | Identifies the event |
| 3. Lock the row | SELECT ... FOR UPDATE in the database | Prevents concurrent processing |
| 4. Check already handled | Look up the ID in the payments table | Detects the duplicate |
| 5. Process exactly once | Credit, mark paid, ship | Single action |
| 6. Respond 200 fast | Acknowledge to stop retries | Cuts the replay loop |
Two crucial details: the unique ID must be constrained UNIQUE in the database (the database becomes the last line of defense against duplicates), and the 200 response must be fast to avoid triggering new retries.
Robust integration checklist
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 of every webhook (Wave, CinetPay, PayDunya each have their own mechanism). Use the aggregator's transaction ID, never your own order number, as the idempotency key. Constrain that ID as UNIQUE. Lock the transaction during processing. Respond 200 immediately and run heavy tasks in a queue. Log every webhook received, processed or ignored, for audit. Test by deliberately replaying the same webhook several times.
Mini case study
Moussa, a developer for an online shop in Thies processing 3,000 orders/month, sees complaints from customers charged twice. Before the fix, about 2% of transactions, or 60 orders/month, caused double processing, at an average basket of 25,000 FCFA: up to 1,500,000 FCFA of potential disputes and refunds each month. He adds a UNIQUE constraint on the transaction ID and a database lock. After deployment, duplicates drop to 0, the support effort on double-charge disputes vanishes, and customer trust recovers. Cost: one day of development.
FAQ
Why receive the same webhook twice? Because aggregators guarantee "at least once" delivery. On a timeout or an error from your server, they replay the event, sometimes 2 to 5 times.
Which key should I use for idempotency? The transaction ID provided by the aggregator, not your internal order number. Constrain it as UNIQUE in the database to guarantee a single processing.
Is the signature enough to avoid duplicates? No. The signature authenticates the sender but says nothing about whether the event was already handled. You must combine signature and idempotency key.
Why respond 200 quickly? Because a slow or error response triggers a new retry. Acknowledge fast, then process heavy tasks (email, shipping) asynchronously.
How do I test my idempotency? Deliberately replay the same webhook several times in a test environment and verify a single order is credited. That is the minimal test before production.
Let's talk about your project. We integrate Wave, CinetPay and PayDunya with idempotency, signature and retry handled end to end. 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.
