Use webhooks to get notified about payment events that happen in your Flutterwave account.

What is a Webhook
At Flutterwave, webhooks are a very important part of our payment process. We use webhooks to communicate with different services. Primarily we use webhooks to share information about an event that happened on your account with you. These events could range from a successful transaction to a failed transaction or even a new debit on your account.

Theoretically, a webhook URL is like a transport medium you specified on your server to receive details about an event that occurred on your Flutterwave account. The event details we send to you contain information about that particular event, including the type of event that occurred and the data associated with it.

This is very useful for events like:

Getting paid via mobile money or USSD where the transaction is completed outside your application.
Recurring billing where an API call is not needed for subsequent billings.
With Flutterwave, you can set up webhooks that would let us notify you anytime events happen on your account. For instance, we can call your webhook URL when:

A user on a subscription is charged,
A customer completes a payment,
We update a pending payment to successful
and so on.
When to use webhooks
Webhooks can be used for all kinds of payment methods, card, account, USSD, Mpesa, and Ghana Mobile money.

If you use Flutterwave to accept alternate payment methods like USSD, Mpesa, and Ghana mobile money, it is best practice to use webhooks so that your account can be notified about changes in the status of the payment once it is completed. This is because these payment methods are asynchronous and responses only come once the customer has completed the payment on their device.

You might also use webhooks to:

Update a customer's membership record in your database when a subscription payment succeeds.

Email a customer when a subscription payment fails.

Update your database when the status of a pending payment is updated to successful.


Don't rely entirely on webhooks

Not in all cases would you be able to rely completely on webhooks to get notified, an example is if your server is experiencing a downtime and your hook endpoints are affected, some customers might still be transacting and the hook calls triggered would fail because your server was unreachable.

In such cases we advise that developers set up a re-query service that goes to poll for the transaction status at regular intervals e.g. every hour using the Verify transaction endpoint, till a successful or failed response is returned.

IMPORTANT NOTE: For every hook call you get, you should endeavour to do a re-query before using the information sent via webhooks. This is to ensure that the data returned has not been compromised in any way before giving value.

Sample webhook responses

On Flutterwave, Webhooks can be configured for all transactions. When a transaction is completed, a POST HTTP request is sent to the webhook URL you have configured. The response may differ depending on the kind of transaction carried out. Here are sample webhook responses for transfers and payments

  "event": "transfer.completed",
  "event.type": "Transfer",
  "data": {
    "id": 33286,
    "account_number": "0690000033",
    "bank_name": "ACCESS BANK NIGERIA",
    "bank_code": "044",
    "fullname": "Bale Gary",
    "created_at": "2020-04-14T16:39:17.000Z",
    "currency": "NGN",
    "debit_currency": "NGN",
    "amount": 30020,
    "fee": 26.875,
    "status": "SUCCESSFUL",
    "reference": "a0a827b1eca65311_PMCKDU_5",
    "meta": null,
    "narration": "lolololo",
    "approver": null,
    "complete_message": "Successful",
    "requires_approval": 0,
    "is_approved": 1
  "event": "charge.completed",
  "data": {
    "id": 285959875,
    "tx_ref": "Links-616626414629",
    "flw_ref": "PeterEkene/FLW270177170",
    "device_fingerprint": "a42937f4a73ce8bb8b8df14e63a2df31",
    "amount": 100,
    "currency": "NGN",
    "charged_amount": 100,
    "app_fee": 1.4,
    "merchant_fee": 0,
    "processor_response": "Approved by Financial Institution",
    "auth_model": "PIN",
    "ip": "",
    "narration": "CARD Transaction ",
    "status": "successful",
    "payment_type": "card",
    "created_at": "2020-07-06T19:17:04.000Z",
    "account_id": 17321,
    "customer": {
      "id": 215604089,
      "name": "Yemi Desola",
      "phone_number": null,
      "email": "[email protected]",
      "created_at": "2020-07-06T19:17:04.000Z"
    "card": {
      "first_6digits": "123456",
      "last_4digits": "7889",
      "country": "NG",
      "type": "VERVE",
      "expiry": "02/23"
  "event": "transfer.completed",
  "event.type": "Transfer",
  "data": {
    "id": 2207648,
    "account_number": "0731702***",
    "bank_name": "ACCESS BANK NIGERIA",
    "bank_code": "044",
    "fullname": "Yemi Desola",
    "created_at": "2020-07-06T21:49:02.000Z",
    "currency": "NGN",
    "debit_currency": "NGN",
    "amount": 5000000000,
    "fee": 53.75,
    "status": "FAILED",
    "reference": "ionn1594072140865",
    "meta": null,
    "narration": "ionnodo",
    "approver": null,
    "complete_message": "DISBURSE FAILED: You can only spend NGN 1000000.00 at once",
    "requires_approval": 0,
    "is_approved": 1
  "transactionId": 777,
  "merchantName": "APPLE/ITUNES",
  "description": "Termination",
  "status": "Successful",
  "balance": 10.0,
  "amount": 10.0,
  "type": "Termination",
  "cardId": "1a4664-***-3838382-***-sd399",
  "maskedPan": "405640******1123"
  "event": "transfer.completed",
  "event.type": "Transfer",
  "data": {
    "id": 15381550,
    "account_number": "82***11",
    "bank_name": "Sterling Bank PLC",
    "bank_code": "232",
    "fullname": "John Doe",
    "created_at": "2021-10-28T08:22:53.000Z",
    "currency": "NGN",
    "debit_currency": "PSA",
    "amount": "50.00",
    "fee": 0,
    "status": "SUCCESSFUL",
    "reference": "PSA_100006211028092157607737423515",
    "meta": null,
    "narration": "WALLET FUNDING",
    "approver": null,
    "complete_message": "",
    "requires_approval": 0,
    "is_approved": 1

The structure is consistent for all payment types, with the last object in the response presenting details about the particular payment type used for payments.

How to setup webhooks on your dashboard.

Follow the following steps to set up a webhook on your Flutterwave account:

  • Login to you Rave dashboard then click on settings
  • On the setting page navigate to webhooks to add your webhook URL
  • Check the all the boxes
  • Save your settings

If you are wondering how to get a webhook URL, you can quickly visit webhook.site to get one for test purposes. In production, your webhook URL should be on your server.

Receiving a webhook notification

Creating a webhook endpoint on your server is no different from creating any page on your website. With PHP, you might create a new .php file on your server; with a framework like Laravel, Flask, Sinatra, you would add a new route with the desired webhook URL.

Hook data is sent as JSON by default, you can change this behavior from your webhook settings page on the dashboard should you prefer to receive it in a different format like form-urlencoded


Checking webhook signatures

You can use a secret hash to verify that the requests you receieved were sent by Flutterwave.

Sample webhook server implementations

// This example uses Express to receive webhooks
const app = require("express");

app.post("/my/webhook/url", function(request, response) {
  /* It is a good idea to log all events received. Add code *
 * here to log the signature and body to db or file       */
  // retrieve the signature from the header
  var hash = req.headers["verif-hash"];
  if(!hash) {
    // discard the request,only a post with the right Flutterwave signature header gets our attention 
  // Get signature stored as env variable on your server
  const secret_hash = process.env.MY_HASH;
  // check if signatures match
  if(hash !== secret_hash) {
   // silently exit, or check that you are passing the right hash on your server.
  // Retrieve the request's body
  var request_json = JSON.parse(request.body);

  // Give value to your customer but don't give any output
// Remember that this is a call from rave's servers and 
// Your customer is not seeing the response here at all


// Retrieve the request's body
$body = @file_get_contents("php://input");

// retrieve the signature sent in the reques header's.
$signature = (isset($_SERVER['HTTP_VERIF_HASH']) ? $_SERVER['HTTP_VERIF_HASH'] : '');

/* It is a good idea to log all events received. Add code *
 * here to log the signature and body to db or file       */

if (!$signature) {
    // only a post with Flutterwave signature header gets our attention

// Store the same signature on your server as an env variable and check against what was sent in the headers
$local_signature = getenv('SECRET_HASH');

// confirm the event's signature
if( $signature !== $local_signature ){
  // silently forget this ever happened

http_response_code(200); // PHP 5.4 or greater
// parse event (which is json string) as object
// Give value to your customer but don't give any output
// Remember that this is a call from rave's servers and 
// Your customer is not seeing the response here at all
$response = json_decode($body);
if ($response->status == 'successful') {
    # code...
    // TIP: you may still verify the transaction
            // before giving value.

Verifying Webhook signature (secret hash)

Flutterwave returns the secret hash configured in your settings as part of your request headers with the key verif-hash. You can store the same secret hash as an environment variable and check if it is the same value sent in the verif-hash property before processing the webhook request. If they are not the same, you can discard the request.

Receiving webhooks with a CSRF-protected server

When using Rails, Django, or any other web framework, your site might automatically check that every POST request contains a CSRF token. This is an important security feature that protects you and your users from cross-site request forgery.

However, this security measure might also prevent your site from processing webhooks sent by Flutterwave. If so, you might need to exempt the webhooks route from CSRF protection. See how to do that below:

import json

# Webhooks are always sent as HTTP POST requests, so we want to ensure
# that only POST requests will reach your webhook view. We can do that by
# decorating `webhook()` with `require_POST`.
# Then to ensure that the webhook view can receive webhooks, we need
# also need to decorate `webhook()` with `csrf_exempt`.
def webhook(request):
  # Process webhook data in `request.body`
class RaveController < ApplicationController
  # If your controller accepts requests other than Rave webhooks,
  # you'll probably want to use `protect_from_forgery` to add CSRF
  # protection for your application. But don't forget to exempt
  # your webhook route!
  protect_from_forgery :except => :webhook

  def webhook
    # Process webhook data in `params`

Responding to a webhook request

To acknowledge receipt of a webhook, your endpoint should return a 200 HTTP status code. All response codes outside this range, including 3xx codes, will indicate to Flutterwave that you did not receive the webhook. This means that a URL redirection or a "Not Modified" response will be treated as a failure. Flutterwave will ignore any other information returned in the request headers or request body.

If your endpoint does not successfully receive a webhook for any reason, be sure to enable all the webhook retry options available on your dashboard under webhook settings

Best practices

If your webhook script performs complex logic or makes network calls, it's possible that the script would time out before Flutterwave sees its complete execution. For that reason, you might want to have your webhook endpoint immediately acknowledge receipt by returning a 200 HTTP status code, and then perform the rest of its duties.

Webhook endpoints might occasionally receive the same event more than once. We advise you to guard against duplicated event receipts by making your event processing idempotent. One way of doing this is logging the events you've processed, and then checking if the status has changed before processing the identical event. Additionally, we recommend verifying webhook signatures to confirm that received events are being sent from Flutterwave.