The verdict in three sentences
A payment webhook is the call the operator sends to your server to say "this payment succeeded"; if it's not verified, an attacker can forge it and trigger a delivery without ever paying. The defense comes in three layers: HMAC signature verification, an idempotency key against replay, and a status re-check directly against the operator's API. Without these layers, fraud is 100% free; with them, it becomes nearly impossible.
The hole: simulating a successful payment
The scenario is mundane. Your code listens on a webhook URL and, on receiving a "status=success" message, marks the order paid and triggers delivery. If you don't verify the message truly comes from the operator, an attacker sends that message themselves with the right order number and walks off with the goods. HMAC signatures solve this: the operator signs each message with a shared secret, and you recompute the signature to confirm the origin.
| Mechanism by operator | Signature header | Method | Risk without verification |
|---|---|---|---|
| Wave | HMAC signature in header | HMAC-SHA256 | Fake payment accepted |
| Paystack | x-paystack-signature | HMAC-SHA512 | Fake payment accepted |
| Flutterwave | verif-hash | Shared secret hash | Fake payment accepted |
| Stripe | Stripe-Signature | HMAC-SHA256 + timestamp | Fake payment accepted |
| Orange Money | Token / signature per integration | Variable | Fake payment accepted |
The golden rule: never trust the webhook body alone. The signature proves the origin, not the business content.
The three layers of defense
Each layer blocks a different attack. Together, they cover forgery, replay and lying about the amount.
| Layer | What it blocks | How |
|---|---|---|
| HMAC signature verification | Forged message | Recompute the hash with the secret |
| Idempotency key | Replay of the same message | Store the event ID, ignore duplicates |
| Status re-check via API | Fake amount / fake status | Query the API with the transaction ID |
| IP allowlist | Unknown source | Only accept operator IPs |
| Timestamp / window | Delayed replay | Reject messages that are too old |
The status re-check is the ultimate safety net: even if everything else fails, querying the operator's API with the transaction ID confirms the true amount and status before delivery.
Idempotency, why it's vital
Operators sometimes resend the same webhook several times (unstable network, automatic retry). Without idempotency, you risk crediting an order twice, shipping goods twice or double-counting revenue. The solution: store the unique identifier of each received event and ignore any duplicate. It's also protection against deliberate replay by an attacker.
Mini case study
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.
Kwame launches an airtime and top-up store in Accra. At first, his webhook accepts any "success" message. In one week, an attacker simulates 40 payments and walks off with 600,000 FCFA of credit. Kwame has the three layers added: HMAC verification (Paystack via x-paystack-signature), idempotency on the event ID, and a status re-check via the API before each delivery. Fix cost: about 300,000 FCFA, one time. Fraud drops to zero. The fix paid for itself in half a prevented attack.
FAQ
Is signature verification enough on its own?
No. It proves the message comes from the operator, but a replay of the same message stays possible without idempotency, and an amount bug can slip through. The combination of signature + idempotency + status re-check is the standard.
Where do I find the signature secret?
In the operator's dashboard (Wave, Paystack, Flutterwave, Stripe), under webhooks or API keys. This secret must stay server-side, never in client code or a public repository.
Do I really need to re-check status via the API?
It's best practice for sensitive amounts. Even with a valid signature, querying the API with the transaction ID guarantees the true status and amount before delivery. It's a fast, cheap call.
Is the IP allowlist mandatory?
No, but it's a free layer: only accepting the IP ranges published by the operator reduces the attack surface. Watch for operators who change their IPs without notice.
How much does securing a webhook cost?
A 2026 order of magnitude: 200,000 to 400,000 FCFA to implement the three layers cleanly on an existing integration. That's negligible against the risk of a single successful attack.
Let's talk about your project. We secure your Wave, Paystack, Flutterwave and Stripe webhooks with HMAC signatures, idempotency and a status re-check. 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.

