Skip to main content

Overview

Webhooks allow you to receive real-time HTTP notifications whenever a transaction status changes. Instead of polling our API for updates, we push the data directly to your server as soon as an event occurs. When a transaction is processed (paid, failed, or cancelled), we send a POST request to the webhook URL you registered with us, containing the full transaction details.
Webhook notifications are sent for terminal transaction statuses: PAID, FAILED, and CANCELLED.

How It Works

1

Register your webhook URL

Set up your webhook callback URL in the dashboard. This is the HTTPS endpoint where we will send event notifications as they occur.
2

A transaction status changes

When a payout transaction reaches a terminal status (PAID, FAILED, or CANCELLED), we build a webhook event with the full transaction details.
3

We send a POST request

Each event is sent individually as a JSON payload via HTTP POST to your registered webhook URL.
4

You acknowledge with 200 OK

Your server must respond with a 200 HTTP status code to confirm receipt. Any other response code triggers a retry.

Event Structure

Each webhook notification is a JSON object with the following fields:

Required Fields

FieldTypeDescription
eventTypeStringType of event. Currently TRANSACTION_STATUS.
ReferenceStringUnique transaction reference (PCN). Encrypted if encryption is enabled.
StatusStringTransaction status: PAID, FAILED, or CANCELLED.
msgNoteStringHuman-readable description of the event.
msgCodeStringStatus code from the processing provider.

Transaction Details

FieldTypeDescription
transactionIdStringInternal transaction identifier.
amountSentNumberAmount sent by the sender.
amountReceivedNumberAmount to be received by the beneficiary.
totalAmountNumberTotal charged amount (sending amount + commission + VAT).
transactionDateStringISO 8601 timestamp of when the transaction was created.
sendingCurrencyCodeStringCurrency of the sending amount (e.g., GBP).
receivingCurrencyCodeStringCurrency of the receiving amount (e.g., NGN).
bankNameStringDestination bank name.
deliveryMethodStringHow the funds are delivered (e.g., ACCOUNTPAYMENT, CASHPICKUP, MOBILE_MONEY).
beneficiaryAccountNumberStringBeneficiary’s bank account number.
beneficiaryAccountHoldersNameStringName on the beneficiary’s bank account.
beneficiaryFullNameStringFull name of the beneficiary.
beneficiaryEmailStringBeneficiary’s email address.
beneficiaryAddressStringBeneficiary’s address.
sortCodeStringBank sort code (where applicable).
bankCodeStringBank code.
countryToStringDestination country name.
transactionTypeStringTransaction type (e.g., ACCOUNTPAYMENT, CASHPICKUP).
paymentTypeStringPayment method used (e.g., CARD).
accountTypeStringBeneficiary account type (e.g., SAVINGS_ACCOUNT).
customerStringSender’s customer ID.
compliantBooleanWhether the transaction passed compliance checks.
complianceCheckStringCompliance check result.
sanctionFlagColorStringSanction screening flag colour.

Example Webhook Payload

Successful Transaction (PAID)

{
  "eventType": "TRANSACTION_STATUS",
  "Reference": "PCN-12345",
  "Status": "PAID",
  "msgNote": "Transaction completed successfully",
  "msgCode": "00",
  "transactionId": "txn-abc-123",
  "amountSent": 500.00,
  "amountReceived": 450.00,
  "totalAmount": 525.50,
  "transactionDate": "2026-02-16T18:58:55.000+00:00",
  "sendingCurrencyCode": "GBP",
  "receivingCurrencyCode": "NGN",
  "bankName": "Access Bank",
  "deliveryMethod": "ACCOUNTPAYMENT",
  "beneficiaryAccountNumber": "0123456789",
  "beneficiaryAccountHoldersName": "John Doe",
  "beneficiaryFullName": "John Doe",
  "beneficiaryEmail": "john@example.com",
  "beneficiaryAddress": "123 Lagos Street, Lagos",
  "sortCode": "123456",
  "bankCode": "044",
  "countryTo": "Nigeria",
  "transactionType": "ACCOUNTPAYMENT",
  "paymentType": "CARD",
  "accountType": "SAVINGS_ACCOUNT",
  "customer": "cust-xyz-789",
  "compliant": true,
  "complianceCheck": "PASSED",
  "sanctionFlagColor": "GREEN"
}

Failed Transaction

{
  "eventType": "TRANSACTION_STATUS",
  "Reference": "PCN-67890",
  "Status": "FAILED",
  "msgNote": "Insufficient funds",
  "msgCode": "0030",
  "transactionId": "txn-def-456",
  "amountSent": 1000.00,
  "amountReceived": 900.00,
  "totalAmount": 1050.00,
  "transactionDate": "2026-02-16T20:30:00.000+00:00",
  "sendingCurrencyCode": "GBP",
  "receivingCurrencyCode": "NGN",
  "bankName": "GTBank",
  "deliveryMethod": "ACCOUNTPAYMENT",
  "beneficiaryFullName": "Jane Smith",
  "countryTo": "Nigeria"
}

Cancelled Transaction

{
  "eventType": "TRANSACTION_STATUS",
  "Reference": "PCN-11111",
  "Status": "CANCELLED",
  "msgNote": "Transaction cancelled by operator",
  "msgCode": "00",
  "transactionId": "txn-ghi-789",
  "amountSent": 250.00,
  "amountReceived": 225.00,
  "totalAmount": 265.00,
  "transactionDate": "2026-02-17T10:15:00.000+00:00",
  "sendingCurrencyCode": "GBP",
  "receivingCurrencyCode": "KES",
  "deliveryMethod": "MOBILE_MONEY",
  "beneficiaryFullName": "Alex Mwangi",
  "countryTo": "Kenya"
}

Status Values

StatusDescription
PAIDTransaction successfully paid out.
FAILEDTransaction failed to process.
CANCELLEDTransaction was cancelled.

Common msgCode Values

CodeDescriptionDetails
00SuccessTransaction completed.
0000PendingTransaction is being processed.
0030Insufficient FundsNot enough balance.
0031Invalid AccountAccount not found.
0099Unknown ErrorGeneric processing error.

Retry Strategy

Your server must respond with a 200 HTTP status code to acknowledge receipt of the webhook. If your server returns any other status code (e.g., 4xx, 5xx) or the request times out, the webhook delivery is considered failed and will be retried.
If your endpoint consistently fails, the message will be retried until the SQS message retention period expires. Ensure your endpoint is reliable and responds promptly.

Handling Webhooks

Respond quickly

Return a 200 status code as fast as possible. Do any heavy processing asynchronously after acknowledging the webhook.

Be idempotent

The same event may be delivered more than once. Use the Reference field to deduplicate and avoid processing the same transaction twice.

Verify the source

See Webhook Security for how to verify that requests are genuinely from our system.

Handle all statuses

Your endpoint should handle PAID, FAILED, and CANCELLED statuses gracefully.

Example Implementation (Node.js)

app.post('/webhook/notify', async (req, res) => {
  const event = req.body;

  if (!event.eventType || !event.Reference || !event.Status) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  const alreadyProcessed = await db.webhookEvents.findOne({
    reference: event.Reference
  });

  if (alreadyProcessed) {
    return res.status(200).json({ status: 'already_processed' });
  }

  res.status(200).json({ status: 'received' });

  try {
    switch (event.Status) {
      case 'PAID':
        await markTransactionAsPaid(event);
        break;
      case 'FAILED':
        await markTransactionAsFailed(event);
        break;
      case 'CANCELLED':
        await markTransactionAsCancelled(event);
        break;
    }

    // store to prevent duplicate processing
    await db.webhookEvents.insert({
      reference: event.Reference,
      eventType: event.eventType,
      status: event.Status,
      processedAt: new Date(),
      payload: event
    });
  } catch (error) {
    console.error('Error processing webhook:', error);
  }
});

Example Implementation (Java / Spring Boot)

@RestController
@RequestMapping("/webhook")
public class WebhookController {

    @PostMapping("/notify")
    public ResponseEntity<Map<String, String>> handleWebhook(@RequestBody Map<String, Object> event) {
        String reference = (String) event.get("Reference");
        String status = (String) event.get("Status");
        String eventType = (String) event.get("eventType");

        if (reference == null || status == null || eventType == null) {
            return ResponseEntity.badRequest()
                .body(Map.of("error", "Missing required fields"));
        }

        if (webhookEventRepository.existsByReference(reference)) {
            return ResponseEntity.ok(Map.of("status", "already_processed"));
        }

        switch (status) {
            case "PAID":
                transactionService.markAsPaid(reference, event);
                break;
            case "FAILED":
                transactionService.markAsFailed(reference, event);
                break;
            case "CANCELLED":
                transactionService.markAsCancelled(reference, event);
                break;
        }

        webhookEventRepository.save(new WebhookEvent(reference, eventType, status));

        return ResponseEntity.ok(Map.of("status", "received"));
    }
}