How Flutterwave webhooks work
When a payment finishes, Flutterwave sends a POST to the webhook URL in your dashboard. For a payment the body looks like{"event": "charge.completed", "data": {"id", "tx_ref", "flw_ref", "amount", "currency", "status", ...}}.tx_ref is the reference you chose when you created the charge; id is Flutterwave's transaction id.
Every request carries a verif-hash header. It is not a signature you compute: it is the secret hash you typed into the dashboard, sent back to you. Compare it with your copy using a constant-time comparison (timingSafeEqual,hash_equals, hmac.compare_digest).
Five rules for a safe handler
- Check
verif-hashfirst. Refuse the request (401) if it is missing or different. - Don't trust the body. Read only
data.id, then callGET /v3/transactions/{id}/verifywith your secret key. - Check the verified transaction. Its
tx_refmust be yours,statusmust besuccessful, the currency must match, and the amount must be at least the order total. - Answer 200 within 60 seconds. Anything else, including a redirect, is a failed delivery. Turn on retries in the dashboard so failures are retried.
- Be idempotent. Events can repeat. Record event type, transaction id and status, and skip what you've already handled.
Don't rely on webhooks alone: Flutterwave recommends checking pending transactions from a scheduled job. You can verify by your own reference with GET /v3/transactions/verify_by_reference?tx_ref=....
Facts from Flutterwave's public v3 documentation (Webhooks and Transaction verification), checked October 2026. The samples are illustrative. Transfer (payout) events are covered more lightly: confirm them with the get-transfer endpoint before you act.
Frequently asked questions
How do I verify a Flutterwave webhook signature?
In the v3 API there is no HMAC to compute. You choose a secret hash in the dashboard's webhook settings, and Flutterwave sends that same value in the verif-hash header. Compare the header with your secret hash using a constant-time comparison, and refuse the request if they differ.
Can I give value as soon as the webhook says "successful"?
No. Flutterwave recommends verifying first: call GET /v3/transactions/{id}/verify with the id from the webhook, and give value only if the tx_ref is yours, status is successful, the currency matches, and the amount is at least what you expected.
What should my webhook endpoint return?
HTTP 200. Flutterwave treats any other status, including 3xx redirects, as a failure, and times out after 60 seconds. If you turn on retries in the dashboard, it retries a failed delivery 3 times, 30 minutes apart.
Why does the same Flutterwave webhook arrive twice?
Retries, and network issues, can deliver an event more than once. Record what you have processed, for example event type plus transaction id plus status, and skip events you have already handled.
Is it safe to paste my secret hash here?
The comparison runs in your browser; the page never sends or stores what you type. If you would rather not paste it, leave it empty: the rest of the checks still work. Never paste your secret API key anywhere.
Related
- Flutterwave mobile money in Uganda with Node.js: charges, verified webhooks, reconciliation and tests.
- Flutterwave mobile money in Uganda with Laravel: a service class, a verify job, a scheduled check and tests.
- Flutterwave mobile money in Uganda with Django: a requests client, locked settlement, a reconcile command and tests.
- African Phone Number Formatter: get Ugandan MTN and Airtel numbers into the right format.
- MoMo Callback Validator: the same checks for MTN MoMo's own API.