Paystack

The Paystack Developer Documentation

Welcome to the Paystack Developer Documentation.

You'll find comprehensive guides and documentation to help you start working with Paystack as quickly as possible, as well as support if you get stuck. Let's jump right in!

Get Started    Discussion

Events

Handle webhooks: Asynchronous value provision. Avoid "Paid but no value" scenarios caused by bad network, user errors, browser errors, integration oversights, etc

Rather than build and manage a system to verify transactions endlessly, we encourage you to embrace our "don't call us, we will call you" maxim. Handle webhooks.

Whenever actions are carried out on your Paystack account, we trigger events which your application can hook into. This is where webhooks come in. You can set this up on your dashboard by specifying a URL we would send POST requests to whenever something interesting happens.

for transactions
Often, a user may have provided all details to conclude payment but processing the payment may timeout, connection issues may occur, callback url redirect may fail in browser etc. To ensure you can give value immediately we confirm processing has completed, please handle webhooks.

for subscriptions
We understand that interacting with a system such as Paystack especially when it comes to subscription payments can introduce the problem of your application being blindsided. You want to be able to tell that a certain invoice payment failed and take necessary steps on your application to either deactivate your customer's subscription, or send them a mail to fund their accounts.

Tips for a good webhook url

  • if using .htaccess, remember to add the trailing / to the url you set.
  • Do a test post to your URL and ensure the script gets the post body.
  • Publicly available url (http://localhost cannot receive!)

Events are created as objects on our end and would be fully queryable in the future. They contain everything about the event, including its type and the data associated with it.

Receiving an event

Creating an endpoint to receive events on your application is as easy as creating a new page that accepts unauthenticated POST requests. The event object is sent as JSON in the request body.

// Using Express
app.post("/my/webhook/url", function(req, res) {
  // Retrieve the request's body
  var event = req.body;
  // Do something with event
  res.send(200);
});
<?php
// Retrieve the request's body and parse it as JSON
$input = @file_get_contents("php://input");
$event = json_decode($input);
// Do something with $event
http_response_code(200); // PHP 5.4 or greater
?>

Confirming events

Since anyone can get hold of your webhook endpoint and attempt to send you phony event objects for malicious purposes (e.g. to see if they can mark their subscription to your product as renewed just in case you aren't running any verifications on the transaction reference), it is important to verify that events originate from Paystack.

You can do any or both of the below to verify events from Paystack:

  • Watch the IPs and accept events only from our IPs below
  • Validate the Signature as decribed in the section that follows

IPs to whitelist

We only call your webhooks from these IPs: Any webhook from outside of these can safely be considered counterfeit:

52.31.139.75
52.49.173.169
52.214.14.220

Valid events are raised with an header X-Paystack-Signature which is essentially a HMAC SHA512 signature of the event payload. Yes, signed using your secret key.

var crypto = require('crypto');
var secret = process.env.SECRET_KEY;
// Using Express
app.post("/my/webhook/url", function(req, res) {
  //validate event
  var hash = crypto.createHmac('sha512', secret).update(JSON.stringify(req.body)).digest('hex');
  if (hash == req.headers['x-paystack-signature']) {
  	// Retrieve the request's body
    var event = req.body;
    // Do something with event  
  }
  res.send(200);
});
import hmac
import hashlib
import json
from django.http import HttpResponse

def processPaystackWebhook(request):
    """
    The function takes an http request object
    containing the json data from paystack webhook client.
    Django's http request and response object was used
    for this example.
    """
    paystack_sk = "sk_fromthepaystackguys"
    json_body = json.loads(request.body)
    computed_hmac = hmac.new(
        bytes(paystack_sk, 'utf-8'),
    str.encode(request.body.decode('utf-8')),
        digestmod=hashlib.sha512
        ).hexdigest()
    if 'HTTP_X_PAYSTACK_SIGNATURE' in request.META:
        if request.META['HTTP_X_PAYSTACK_SIGNATURE'] == computed_hmac:
            #IMPORTANT! Handle webhook request asynchronously!!
            #
            #..code
            #
            return HttpResponse(status=200)
    return HttpResponse(status=400) #non 200
<?php
// only a post with paystack signature header gets our attention
if ((strtoupper($_SERVER['REQUEST_METHOD']) != 'POST' ) || !array_key_exists('HTTP_X_PAYSTACK_SIGNATURE', $_SERVER) ) 
    exit();

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

// validate event do all at once to avoid timing attack
if($_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] !== hash_hmac('sha512', $input, PAYSTACK_SECRET_KEY))
  exit();

http_response_code(200);

// parse event (which is json string) as object
// Do something - that will not take long - with $event
$event = json_decode($input);

exit();
package com.paystack;

class PaystackAuthValidator {
    
    private static final String ALGORITHM = "HmacSHA512";
    
    public static boolean isTokenValid(String rawJsonRequest, String authToken, String secretKey) throws Exception {
        Mac mac = getMac(secretKey);
        final byte[] mac_data = mac.doFinal(convertBodyToBytes(rawJsonRequest));
        
        String result = DatatypeConverter.printHexBinary(mac_data);
        return result.toLowerCase().equals(authToken);
    }
    
    private static Mac getMac(String secretKey) throws Exception {
        Mac mac = Mac.getInstance(ALGORITHM);
        mac.init(getSecretKeySpec(ALGORITHM, secretKey));
        return mac;
    }
    
    private static SecretKeySpec getSecretKeySpec(String algorithm, String secretKey) throws Exception {
        byte[] byteKey = secretKey.getBytes("UTF-8");
        return new SecretKeySpec(byteKey, algorithm);
    }
    
    private static byte[] convertBodyToBytes(String rawJson) throws UnsupportedEncodingException {
        return new JSONObject(rawJson).toString().getBytes("UTF-8");
    }
}

// test included below (replace with a sample so you can have test coverage for the file above

/************/


package com.paystack;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

class PaystackAuthValidatorTest {
    
    private static final String TEST_SECRET_KEY = "sk_test_somesecret";
    
    
    private static final String CHARGE_SUCCESS_TOKEN = "sometoken";
    private static final String CHARGE_SUCCESS_JSON =
            "{\"some\":\"json\"}";
    
    
    @Test
    public void checkCorrectnessOfTolenValidatonForSuccessCharge() throws Exception {
        assertThat(PaystackAuthValidator.isTokenValid(
                CHARGE_SUCCESS_JSON, CHARGE_SUCCESS_TOKEN, TEST_SECRET_KEY
        )).isTrue();
    }
    
}
using System;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json.Linq;

namespace HMacExample
{
    class Program
    {
        static void Main(string[] args)
        {
            String key = "YOUR_SECRET_KEY"; //replace with your paystack secret_key
            String jsonInput = "{\"paystack\":\"request\",\"body\":\"to_string\"}"; //the json input
            String inputString = Convert.ToString(new JValue(jsonInput));

            String result = "";
            byte[] secretkeyBytes = Encoding.UTF8.GetBytes(key);
            byte[] inputBytes = Encoding.UTF8.GetBytes(inputString);
            using (var hmac = new HMACSHA512(secretkeyBytes))
            {
                byte[] hashValue = hmac.ComputeHash(inputBytes);
                result = BitConverter.ToString(hashValue).Replace("-", string.Empty);;
            }
            Console.WriteLine(result);

            String xpaystackSignature = ""; //put in the request's header value for x-paystack-signature
        
            if(result.ToLower().Equals(xpaystackSignature)) {
                // you can trust the event, it came from paystack
                // respond with the http 200 response immediately before attempting to process the response
                //retrieve the request body, and deliver value to the customer
            } else {
                // this isn't from Paystack, ignore it
            }
        }
    }
}

Responding to an event

You should respond to an event with a 200 OK. We consider this an acknowledgement by your application. If your application responds with any status outside of the 2xx range, we will consider it unacknowledged and thus, continue to send it every hour for 72 hours. You don't need to send a request body or some other parameter as it would be discarded - we only pay attention to the status code.

If your application is likely to start a long running task in response to the event, Paystack may timeout waiting for the response and would ultimately consider the event unacknowledged and queue to be raised later. You can mitigate duplicity by having your application respond immediately with a 200 before it goes on to perform the rest of the task.

Structure of an event object

An event object is sent in JSON and similar to what you would get in response to a typical API request. Well, the data bit of it. Below is the body of an event that was fired when we created a subscription following this example. The other tabs highlight the structure of other types of events.


{
  "event": "subscription.create",
  "data": {
    "domain": "test",
    "status": "active",
    "subscription_code": "SUB_vsyqdmlzble3uii",
    "amount": 50000,
    "cron_expression": "0 0 28 * *",
    "next_payment_date": "2016-05-19T07:00:00.000Z",
    "open_invoice": null,
    "createdAt": "2016-03-20T00:23:24.000Z",
    "plan": {
      "name": "Monthly retainer",
      "plan_code": "PLN_gx2wn530m0i3w3m",
      "description": null,
      "amount": 50000,
      "interval": "monthly",
      "send_invoices": true,
      "send_sms": true,
      "currency": "NGN"
    },
    "authorization": {
      "authorization_code": "AUTH_96xphygz",
      "bin": "539983",
      "last4": "7357",
      "exp_month": "10",
      "exp_year": "2017",
      "card_type": "MASTERCARD DEBIT",
      "bank": "GTBANK",
      "country_code": "NG",
      "brand": "MASTERCARD"
    },
    "customer": {
      "first_name": "BoJack",
      "last_name": "Horseman",
      "email": "bojack@horsinaround.com",
      "customer_code": "CUS_xnxdt6s1zg1f4nx",
      "phone": "",
      "metadata": {},
      "risk_action": "default"
    },
    "created_at": "2016-10-01T10:59:59.000Z"
  }
}
{
  "event": "invoice.update",
  "data": {
    "domain": "test",
    "invoice_code": "INV_kmhuaaur5c9ruh2",
    "amount": 50000,
    "period_start": "2016-04-19T07:00:00.000Z",
    "period_end": "2016-05-19T07:00:00.000Z",
    "status": "success",
    "paid": true,
    "paid_at": "2016-04-19T06:00:09.000Z",
    "description": null,
    "authorization": {
      "authorization_code": "AUTH_jhbldlt1",
      "bin": "539923",
      "last4": "2071",
      "exp_month": "10",
      "exp_year": "2017",
      "card_type": "MASTERCARD DEBIT",
      "bank": "FIRST BANK OF NIGERIA PLC",
      "country_code": "NG",
      "brand": "MASTERCARD"
    },
    "subscription": {
      "status": "active",
      "subscription_code": "SUB_l07i1s6s39nmytr",
      "amount": 50000,
      "cron_expression": "0 0 19 * *",
      "next_payment_date": "2016-05-19T07:00:00.000Z",
      "open_invoice": null
    },
    "customer": {
      "first_name": "BoJack",
      "last_name": "Horseman",
      "email": "bojack@horsinaround.com",
      "customer_code": "CUS_xnxdt6s1zg1f4nx",
      "phone": "",
      "metadata": {},
      "risk_action": "default"
    },
    "transaction": {
      "reference": "rdtmivs7zf",
      "status": "success",
      "amount": 50000,
      "currency": "NGN"
    },
    "created_at": "2016-04-16T13:45:03.000Z"
  }
}
{
  "event": "charge.success",
  "data": {
    "id": 84,
    "domain": "test",
    "status": "success",
    "reference": "9cfbae6e-bbf3-5b41-8aef-d72c1a17650g",
    "amount": 50000,
    "message": null,
    "gateway_response": "Approved",
    "paid_at": "2018-12-20T15:00:06.000Z",
    "created_at": "2018-12-20T15:00:05.000Z",
    "channel": "card",
    "currency": "NGN",
    "ip_address": null,
    "metadata": {
      "custome_fields": [
        {
          "display_name": "A sample",
          "variable_name": "a_sample",
          "value": "custom field"
        }
      ]
    },
    "log": null,
    "fees": 750,
    "fees_split": null,
    "authorization": {
      "authorization_code": "AUTH_9246d0h9kl",
      "bin": "408408",
      "last4": "4081",
      "exp_month": "12",
      "exp_year": "2020",
      "channel": "card",
      "card_type": "visa DEBIT",
      "bank": "Test Bank",
      "country_code": "NG",
      "brand": "visa",
      "reusable": true,
      "signature": "SIG_iCw3p0rsG7LUiQwlsR3t"
    },
    "customer": {
      "id": 4670376,
      "first_name": "Asample",
      "last_name": "Personpaying",
      "email": "asam@ple.com",
      "customer_code": "CUS_00w4ath3e2ukno4",
      "phone": "",
      "metadata": null,
      "risk_action": "default"
    },
    "plan": {
      "id": 17,
      "name": "A s(i/a)mple plan",
      "plan_code": "PLN_dbam2fwcqkyyfjc",
      "description": "Sample plan for docs",
      "amount": 50000,
      "interval": "hourly",
      "send_invoices": true,
      "send_sms": true,
      "currency": "NGN"
    },
    "subaccount": {},
    "paidAt": "2018-12-20T15:00:06.000Z"
  }
}
{
  event: "transfer.success",
  data: {
    domain: "live",
    amount: 10000,
    currency: "NGN",
    source: "balance",
    source_details: null,
    reason: "Bless you",
    reference: "JDJDJ",
    recipient: {
      domain: "live",
      type: "nuban",
      currency: "NGN",
      name: "Someone",
      details: {
        account_number: "0123456789",
        account_name: null,
        bank_code: "058",
        bank_name: "Guaranty Trust Bank"
      },
      description: null,
      metadata: null,
      recipient_code: "RCP_xoosxcjojnvronx",
      active: true
    },
    status: "success",
    transfer_code: "TRF_zy6w214r4aw9971",
    transferred_at: "2017-03-25T17:51:24.000Z",
    created_at: "2017-03-25T17:48:54.000Z"
  }
}
{
  "event": "transfer.failed",
  "data": {
    "domain": "test",
    "amount": 10000,
    "currency": "NGN",
    "source": "balance",
    "source_details": null,
    "reason": "Test",
    "reference": "XJKSKS",
    "recipient": {
      "domain": "live",
      "type": "nuban",
      "currency": "NGN",
      "name": "Test account",
      "details": {
        "account_number": "0000000000",
        "account_name": null,
        "bank_code": "058",
        "bank_name": "Zenith Bank"
      },
      "description": null,
      "metadata": null,
      "recipient_code": "RCP_7um8q67gj0v4n1c",
      "active": true
    },
    "status": "failed",
    "transfer_code": "TRF_3g8pc1cfmn00x6u",
    "transferred_at": null,
    "created_at": "2017-12-01T08:51:37.000Z"
  }
}
{
  "event": "invoice.create",
  "data": {
    "domain": "test",
    "invoice_code": "INV_thy2vkmirn2urwv",
    "amount": 50000,
    "period_start": "2018-12-20T15:00:00.000Z",
    "period_end": "2018-12-19T23:59:59.000Z",
    "status": "success",
    "paid": true,
    "paid_at": "2018-12-20T15:00:06.000Z",
    "description": null,
    "authorization": {
      "authorization_code": "AUTH_9246d0h9kl",
      "bin": "408408",
      "last4": "4081",
      "exp_month": "12",
      "exp_year": "2020",
      "channel": "card",
      "card_type": "visa DEBIT",
      "bank": "Test Bank",
      "country_code": "NG",
      "brand": "visa",
      "reusable": true,
      "signature": "SIG_iCw3p0rsG7LUiQwlsR3t"
    },
    "subscription": {
      "status": "active",
      "subscription_code": "SUB_fq7dbe8tju0i1v8",
      "email_token": "3a1h7bcu8zxhm8k",
      "amount": 50000,
      "cron_expression": "0 * * * *",
      "next_payment_date": "2018-12-20T00:00:00.000Z",
      "open_invoice": null
    },
    "customer": {
      "id": 46,
      "first_name": "Asample",
      "last_name": "Personpaying",
      "email": "asam@ple.com",
      "customer_code": "CUS_00w4ath3e2ukno4",
      "phone": "",
      "metadata": null,
      "risk_action": "default"
    },
    "transaction": {
      "reference": "9cfbae6e-bbf3-5b41-8aef-d72c1a17650g",
      "status": "success",
      "amount": 50000,
      "currency": "NGN"
    },
    "created_at": "2018-12-20T15:00:02.000Z"
  }
}

Types of events

Here are the events we currently raise. We would add more to this list as we hook into more actions in the future.

Event name
Event description

subscription.create

A subscription has been created

subscription.disable

A subscription on your account has been disabled

subscription.enable

A subscription on your account has been enabled

invoice.create

An invoice has been created for a subscription on your account. This usually happens 3 days before the subscription is due or whenever we send the customer their first pending invoice notification

invoice.update

An invoice has been updated. This usually means we were able to charge the customer successfully. You should inspect the invoice object returned and take necessary action

charge.success

A successful charge was made.

transfer.success

A successful transfer has been completed.

transfer.failed

A transfer you attempted has failed.