Mantissa Hooksnode is a Mantissa product A Mantissa product mantissatech.xyz

Why your Flutterwave webhook is not arriving (and how to fix it)

Author:
hooksnode
Published:
Updated:
  • flutterwave
  • webhooks
  • troubleshooting

Your customer paid, Flutterwave shows the transaction as successful, and your app never heard about it. Most of the time the cause is one of five things. This guide walks through each one, shows how to fix it, and explains how to recover the events you missed.

The Flutterwave facts below come from Flutterwave's webhook docs.

Check the delivery record first

Before you change code, open the transaction in the Flutterwave dashboard and look at its webhook delivery. It tells you whether Flutterwave tried to send the event, which URL it used and what your server answered. A wrong URL, a test URL on a live account, or a status code other than 200 points you straight at one of the causes below.

Cause 1: the verif-hash does not match

Flutterwave does not compute a signature. You set a secret hash in the dashboard, and Flutterwave sends that same value back in the verif-hash header on every webhook. Your server checks that the header equals the hash it has stored. If your check fails, your handler rejects a real event.

Common reasons it fails:

  • Trailing spaces or newlines in the stored value, often from copying it into an env file.
  • Test vs live. The test and live dashboards each have their own settings. Make sure the hash your server reads matches the mode the event came from.
  • The wrong env var. The hash is not your secret key or your public key. It is the value you typed into the secret hash field.

Compare in constant time so the check does not leak timing:

// Node.js
import crypto from 'node:crypto';

function verifyFlutterwave(req) {
  const expected = Buffer.from(process.env.FLW_SECRET_HASH.trim());
  const got = Buffer.from(String(req.headers['verif-hash'] || ''));
  return got.length === expected.length && crypto.timingSafeEqual(got, expected);
}
<?php
// PHP
$expected = trim(getenv('FLW_SECRET_HASH'));
$got = $_SERVER['HTTP_VERIF_HASH'] ?? '';
if (!hash_equals($expected, $got)) {
    http_response_code(401);
    exit;
}
# Python
import hmac, os

def verify_flutterwave(headers):
    expected = os.environ["FLW_SECRET_HASH"].strip()
    got = headers.get("verif-hash", "")
    return hmac.compare_digest(got, expected)

Cause 2: a redirect

Flutterwave counts only 200 as success. A 301, 302 or 308 is a failure, even if the page it points to would answer 200. Common sources of redirects:

  • The URL uses http:// and your server redirects to https://.
  • A trailing slash is added or removed by your framework.
  • Your host redirects the bare domain to www or the other way round.

Fix it by pasting the final URL into the dashboard. Test with curl -i -X POST <your-url> and check that the first line is not a 3xx.

Cause 3: a slow handler

Flutterwave times out after 60 seconds. If your handler calls other services, writes to slow storage or sends emails before it answers, a busy moment can push it past that limit.

Answer 200 as soon as you have checked the hash and saved the event. Do the slow work in a background job.

Cause 4: retries are turned off

Flutterwave retries a failed webhook only if Enable Webhook retries is turned on under Settings, Webhooks. With it on, Flutterwave retries 3 times, 30 minutes apart. That is about 90 minutes in total. A deploy that goes wrong at night, or a server that is down for two hours, can miss every attempt.

Turn retries on, and plan for the case where all of them fail (see recovering missed events below).

Cause 5: duplicates you treated as new

Retries mean the same event can arrive more than once, and Flutterwave says it can send duplicates. If your handler is not idempotent, a duplicate can look like a second payment, or a unique key error can make your handler answer with an error, which Flutterwave then counts as a failure.

Pick one key and store it with a unique constraint:

  • Flutterwave suggests event plus data.id plus data.status.
  • Hooksnode uses data.tx_ref as the idempotency key for Flutterwave events.

If the key is already stored, answer 200 and do nothing else.

Recovering missed events

  • Resend through the API. Flutterwave has a resend failed webhook endpoint for completed transactions. The dashboard resend button works for transfer (payout) webhooks only. For collections, use the API. See Can a failed webhook be resent?
  • Reconcile. Do not rely on webhooks alone for money. Before you give value, or in a scheduled job, call Flutterwave's transaction verify endpoint and compare the result with your records.

With Hooksnode

Hooksnode can sit between Flutterwave and your servers so a bad deploy on your side does not lose events.

  • One URL, Flutterwave preset. Point Flutterwave at one Hooksnode source URL and pick the Flutterwave signature preset. Hooksnode checks the verif-hash header and answers 401 on a bad value.
  • Answers fast, retries for longer. Hooksnode saves the event and answers right away, then delivers to each of your destinations on its own schedule: up to 25 attempts over about 17.5 hours, with the wait capped at 1 hour. See retries.
  • See what arrived. The event inspector shows each event Hooksnode accepted. If the event is there and your server did not get it, the problem is between Hooksnode and your server, and each attempt shows what your server answered.
  • Retry or replay after the fix. Once your server is fixed, retry the dead deliveries or replay the event. See retry and replay.
  • At least once, never exactly once. Hooksnode can also deliver an event more than once, so keep your handlers idempotent.

One gotcha: a Free project with no destination answers 400. Add a destination before you point Flutterwave at it.

Send one event to several services

Follow Fan out Flutterwave webhooks when one Flutterwave event has to reach more than one service.

Using Paystack too?

Try it free

The project Free plan is $0 and includes 100,000 credits a month with a hard cap: when credits run out, new events get 402 instead of a bill. Start free.

Further reading