Events (Webhooks)

User webhooks to get notified about payment events that happen in your Rave account.

What is a Webhook

A WebHook is an HTTP callback: an HTTP POST that occurs when something happens; a simple event-notification via HTTP POST. A web application implementing WebHooks will POST a message to a URL when certain things happen.

Rave sends webhooks events that notify your application any time a payment event happens in your account. 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.

In Rave you can setup webhooks that would let us notify you anytime events- A user on a subscription is charged, a customer completes a payment, we update a pending payment to successful- happen in your account.

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 Rave to accept alternate payment methods like USSD, Mpesa, and Ghana mobile money, it is best practice to use webhooks so that your integration can be notified about changes 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.

NB: 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 independently of that and the hook call 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 Payment endpoint, till a successful or failed response is returned.

Sample Transaction Payload

On Rave, Webhooks can be configured for transactions. When a transaction is completed, a POST HTTP request is sent to the URL you have configured. The HTTP payload will contain

Hook Structure

The hook request structure is consistent across the board, but you can differentiate the event type using the event.type parameter returned. See the list of possible values for the parameter below:

Zambia Mobile Money: MOBILEMONEYZM_TRANSACTION

Uganda Mobile Money: MOBILEMONEYUG_TRANSACTION

Ghana Mobile Money: MOBILEMONEYGH_TRANSACTION

Rwanda Mobile Money: MOBILEMONEYRW_TRANSACTION

Card Payments: CARD_TRANSACTION

M-Pesa Payments: MPESA_TRANSACTION

QR Payments: MVISA-QR_TRANSACTION

Barter Payments: BARTER_TRANSACTION

Pay with Bank Transfer: BANK_TRANSFER_TRANSACTION

USSD: USSD_TRANSACTION

PayAttitude: PAYATTITUDE_TRANSACTION

ACCOUNT/ACH: ACCOUNT_TRANSACTION

Francophone Mobile Money: MOBILEMONEYSN_TRANSACTION

Payouts: Transfer

E-Bills: EBILLS_TRANSACTION

{
  "id": 126122,
  "txRef": "rave-pos-121775237991",
  "flwRef": "FLW-MOCK-72d0b2d66273fad0bb32fdea9f0fa298",
  "orderRef": "URF_1523185223111_833935",
  "paymentPlan": null,
  "createdAt": "2018-04-08T11:00:23.000Z",
  "amount": 1000,
  "charged_amount": 1000,
  "status": "successful",
  "IP": "197.149.95.62",
  "currency": "NGN",
  "customer": {
    "id": 22836,
    "phone": null,
    "fullName": "Anonymous customer",
    "customertoken": null,
    "email": "[email protected]",
    "createdAt": "2018-04-08T11:00:22.000Z",
    "updatedAt": "2018-04-08T11:00:22.000Z",
    "deletedAt": null,
    "AccountId": 134
  },
  "entity": {
    "card6": "539983",
    "card_last4": "8381"
  },
  "event.type": "CARD_TRANSACTION"
}
{
  "id": 125837,
  "txRef": "rave-pos-272519815315",
  "flwRef": "FLWACHMOCK-1523118279396",
  "orderRef": "URF_1523118277202_7343035",
  "paymentPlan": null,
  "createdAt": "2018-04-07T16:24:37.000Z",
  "amount": 200,
  "charged_amount": 200,
  "status": "successful",
  "IP": "197.149.95.62",
  "currency": "NGN",
  "customer": {
    "id": 5766,
    "phone": "N/A",
    "fullName": "Anonymous customer",
    "customertoken": null,
    "email": "[email protected]",
    "createdAt": "2017-10-16T10:03:19.000Z",
    "updatedAt": "2017-10-16T10:03:19.000Z",
    "deletedAt": null,
    "AccountId": 134
  },
  "entity": {
    "account_number": "0690000037",
    "first_name": "Dele Moruf",
    "last_name": "Quadri"
  },
 "event.type": "ACCOUNT_TRANSACTION"
}
{
  "id": 560930,
  "txRef": "MC-1556614529471",
  "flwRef": "flwm3s4m0c1556614533770",
  "orderRef": "URF_MMGH_1556614532854_4300235",
  "paymentPlan": null,
  "createdAt": "2019-04-30T08:55:32.000Z",
  "amount": 50,
  "charged_amount": 50.72,
  "status": "successful",
  "IP": "::ffff:10.63.55.145",
  "currency": "GHS",
  "customer": {
    "id": 112307,
    "phone": "08082000503",
    "fullName": "Anonymous Customer",
    "customertoken": null,
    "email": "[email protected]",
    "createdAt": "2019-04-04T15:40:22.000Z",
    "updatedAt": "2019-04-04T15:40:22.000Z",
    "deletedAt": null,
    "AccountId": 8364
  },
  "entity": {
    "id": "NO-ENTITY"
  },
  "event.type": "MOBILEMONEYGH_TRANSACTION"
}
{
  "id": 130438,
  "txRef": "rave-1902008383",
  "flwRef": "ws_CO_15042018193205498_1998935614_1884_1523809926391",
  "orderRef": "1998935614_1884_1523809926391",
  "paymentPlan": null,
  "createdAt": "2018-04-15T16:32:06.000Z",
  "amount": 2000,
  "charged_amount": 2028,
  "status": "successful",
  "IP": "41.86.149.34",
  "currency": "KES",
  "customer": {
    "id": 23858,
    "phone": "254791498442",
    "fullName": "Anonymous customer",
    "customertoken": null,
    "email": "[email protected]",
    "createdAt": "2018-04-15T16:32:05.000Z",
    "updatedAt": "2018-04-15T16:32:05.000Z",
    "deletedAt": null,
    "AccountId": 1884
  },
  "entity": {
    "id": "NO-ENTITY"
  },
 "event.type": "MPESA_TRANSACTION"
}
{
  "event.type": "Transfer",
  "transfer": {
    "id": 570,
    "account_number": "0690000040",
    "bank_code": "044",
    "fullname": "Alexis Sanchez",
    "date_created": "2018-06-11T14:07:49.000Z",
    "currency": "NGN",
    "amount": 9000,
    "fee": 45,
    "status": "SUCCESSFUL",
    "reference": "rave-transfer-152812343460966",
    "narration": "New transfer",
    "approver": null,
    "complete_message": "Approved Or Completed Successfully",
    "requires_approval": 0,
    "is_approved": 1,
    "bank_name": "ACCESS BANK NIGERIA"
  }
}
{
  "id": 473055,
  "txRef": "rave-123456",
  "flwRef": "FLW-MOCK-RECURR-3c316e36c65da2f7cd4bfb88f6977a51",
  "orderRef": "URF_1551966008798_3588435",
  "paymentPlan": null,
  "createdAt": "2019-03-07T13:40:08.000Z",
  "amount": 5000,
  "charged_amount": 5000,
  "status": "successful",
  "IP": "223.72.120.42",
  "currency": "NGN",
  "customer": {
    "id": 4222,
    "phone": "08035134649",
    "fullName": "Temi Adelewa",
    "customertoken": null,
    "email": "[email protected]",
    "createdAt": "2017-09-19T23:03:32.000Z",
    "updatedAt": "2017-09-19T23:03:32.000Z",
    "deletedAt": null,
    "AccountId": 134
  },
  "entity": {
    "card6": "543889",
    "card_last4": "0229"
  },
  "event.type": "CARD_TRANSACTION"
}
{
  "id": 68376907,
  "txRef": "Rave-Pages374737616222",
  "flwRef": "439695021",
  "orderRef": "URF_1563264772390_6617735",
  "paymentPlan": null,
  "createdAt": "2019-07-16T08:17:35.000Z",
  "amount": 101.5,
  "charged_amount": 101.5,
  "status": "successful",
  "IP": "41.190.30.39",
  "currency": "NGN",
  "customer": {
    "id": 42218458,
    "phone": null,
    "fullName": "Tens Ani",
    "customertoken": null,
    "email": "[email protected]",
    "createdAt": "2019-07-16T08:12:52.000Z",
    "updatedAt": "2019-07-16T08:12:52.000Z",
    "deletedAt": null,
    "AccountId": 48
  },
  "entity": {
    "id": "NO-ENTITY"
  },
  "event.type": "BANK_TRANSFER_TRANSACTION"
}
{
  "id": 473055,
  "txRef": "E147DFDAD142B78F",
  "flwRef": "RVEBLS-DB92D90FEAEA-2",
  "orderRef": "URF_1551966008798_3588435",
  "paymentPlan": null,
  "createdAt": "2019-03-07T13:40:08.000Z",
  "amount": 5000,
  "charged_amount": 5000,
  "status": "successful",
  "IP": "223.72.120.42",
  "currency": "NGN",
  "customer": {
    "id": 4222,
    "phone": "08035134649",
    "fullName": "Temi Adelewa",
    "customertoken": null,
    "email": "[email protected]",
    "createdAt": "2017-09-19T23:03:32.000Z",
    "updatedAt": "2017-09-19T23:03:32.000Z",
    "deletedAt": null,
    "AccountId": 134
  },
  "entity": {
     "id": "NO-ENTITY"
  },
  "event.type": "EBILLS_TRANSACTION"
}

How to setup webhooks on your dashboard.

1902

Login to you Rave dashboard then click on settings , on the setting page navigate to webhooks to add a webhook.

2752

Once on the webhook page, click the input text to add your webhook and use the save action button to save it.

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.

Webhook data is sent as form-urlencoded by default but you can configure on your webhook settings page on the dashboard to send the request as JSON instead.

👍

Checking webhook signatures

You can use a secret hash to verify that your received requests were sent by rave.

<?php

// 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 rave signature header gets our attention
    exit();
}

// 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
  exit();
}

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.
}
exit();
// 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 rave 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 write 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

  response.send(200);
});
require "json"

# Using Sinatra
post "/my/webhook/url" do
  # Retrieve the request's body
  request_json = request.body.read

  # Do something with request_json

  status 200
end
mport json
from django.http import HttpResponse

# Using Django
def my_webhook_view(request):
  # Retrieve the request's body
  request_json = request.body

  # Do something with request_json

  return HttpResponse(status=200)
// Using Spark framework (http://sparkjava.com)
public Object handle(Request request, Response response) {
  // Retrieve the request's body and parse it as JSON
  Request requestJson = APIResource.GSON.fromJson(request.body(), Event.class);

  // Do something with eventJson

  response.status(200);
  return "";
}
using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;

namespace workspace.Controllers
{
    [Route("api/[controller]")]
    public class RaveWebHook : Controller
    {
        [HttpPost]
        public void Index() {
          	
            var Ravejson = new StreamReader(HttpContext.Request.Body).ReadToEnd();
            

            // Do something with Ravejson
        }
    }
}

Verifying Webhook signature (secret hash)

Rave returns the secret hash configured in your settings, in the request headers as 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 rave. 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`.
@require_POST
@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`
  end
end

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 Rave that you did not receive the webhook. This does mean that a URL redirection or a "Not Modified" response will be treated as a failure. Rave 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, webhooks would not be retried, though you can query for the status using the Verify Payment endpoint to reconcile your data with any missed events.

Best practices

If your webhook script performs complex logic, or makes network calls, it's possible that the script would time out before rave 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 Rave.


Next Steps

When you receive a successful hook request you can verify the transaction to give value.