Build With MyBundlePay

One integration for payments, cards, bills and payouts.

MyBundlePay gives developers a clean payment gateway, virtual account funding, payout rails, card issuing tools, bill payments, webhooks and merchant-ready checkout experiences.

REST APIs JSON based
Webhooks Real time events
Sandbox Test first

API Test Console

Test the current endpoint directly from the documentation. Secret keys stay inside this browser session.

Enter your test key, review the sample body for this page, then send a request.

Webhooks are an important part of your payment integration. They allow MyBundlePay notify you about events that happen on your account, such as a charge or payment transaction. A webhook URL is an endpoint on your server where you can receive notifications about such events. When an event occurs, we'll make a POST request to that endpoint, with a JSON body containing the details about the event, including the type of event and the data associated with it.

Enabling webhooks

Here's how to set up a webhook on your MyBundlePay account:

  1. Log in to your dashboard and click on Settings
  2. Navigate to Webhooks to add your webhook URL
  3. Check all the boxes and save your settings

Verifying webhook signatures

When enabling webhooks, you have to set a webhook secret. Since webhook URLs are publicly accessible, the webhook secret allows you to verify that incoming requests are from MyBundlePay. You can specify any value as your secret hash, but we recommend something random. You should also store it as an environment variable on your server. You must specify a webhook secret, as we'll include it in our request to your webhook URL, in a header called Signature. In the webhook endpoint, check if the Signature header is present and that it matches the secret hash you set. If the header is missing, or the value doesn't match, you can discard the request, as it isn't from MyBundlePay.

Responding to webhook requests

To acknowledge receipt of a webhook, your endpoint must return a 200 HTTP status code. Any other response codes, including 3xx codes, will be treated as a failure. We don't care about the response body or headers

You may need to disable CSRF protection

Some web frameworks like Rails or Django, automatically check that every POST request contains a CSRF token. This is a useful security feature that protects you and your users from cross-site request forgery.

Example

Here are a few examples of implementing a webhook endpoint in php

              
              // In a Laravel-like app:
              Route::post('webhook', function (\Illuminate\Http\Request $request) {
                  //check for the signature
                  $secret = 'mybundlepay';
                  $signature = $request->header('webhook-secret');
                  $sign_secret = hash_hmac('sha256', json_encode($request->all()), $secret);
                  if (!$signature || ($signature !== $sign_secret)) {
                      // This request isn't from MyBundlePay; discard
                      abort(401);
                  }
                  $payload = $request->all();
                  // It's a good idea to log all received events.
                  Log::info($payload);
                  // Do something (that doesn't take too long) with the payload
                  return response(200);
              });
              
            

Retries & Failure

In a case where MyBundlePay was unable to reach the URL, all the webhooks will be retried. Webhook URLs must respond with a status 200 OK or the request will be considered unsuccessful and retried.

Sample webhook format

The JSON payload below is a sample response of a webhook event that gets sent to your webhook URL. You should know that all webhook events across MyBundlePay, payout, funding, payouts all follow the same payload format as shown below.

Fields Descriptions
event The name or type of webhook event that gets sent eg; charge.
data The payload of the webhook event object that gets sent.

Webhook events

Event Descriptions
charge Payment event.
payout Payout event.
and
payout.finalized Sent when a payout reaches its final SUCCESS or FAILED status.
funding Account funding event.

Payout Webhook

When a payout or withdrawal reaches its final state, MyBundlePay will send a POST request to your configured webhook URL.

The webhook will be sent when the payout is either SUCCESS or FAILED.

Important: Always verify the webhook signature before updating the transaction status on your system.

Webhook Headers

Header Description
Content-Type application/json
X-Webhook-Signature HMAC SHA256 signature generated using your webhook secret.
X-Webhook-Event payout.finalized
X-Webhook-ID Unique identifier generated for the webhook event.

Successful Payout Webhook

When the payout is completed successfully, you will receive a payload similar to the example below.


{
    "event": "payout.finalized",
    "event_id": "payout_5c895e1e28b405fabfe208dc976c24b1ac3fdfc3c9e025ec5accac4e17727aa1",
    "status": "success",
    "message": "Payout completed successfully.",
    "data": {
        "reference": "WD-78HRRM73SH4RY3X3",
        "amount": "890.00",
        "transaction_status": "SUCCESS",
        "bank_name": "PALMPAY",
        "account_number": "0001111000",
        "account_name": "MYBUNDLEPAY",
        "transaction_type": "withdrawal"
    },
    "timestamp": "2026-08-26T14:05:48+01:00"
}

    

Failed Payout Webhook

If the payout cannot be completed and reaches a final failed state, you will receive a payload similar to the example below.


{
    "event": "payout.finalized",
    "event_id": "payout_2b8df7191ce4845d43d738ef4fe7325832b18048ff15582334e515b5d51fe2d1",
    "status": "failed",
    "message": "Payout failed.",
    "data": {
        "reference": "WD-78HRRM73SH4RY3X3",
        "amount": "890.00",
        "transaction_status": "FAILED",
        "bank_name": "PALMPAY",
        "account_number": "0001111000",
        "account_name": "MYBUNDLEPAY",
        "transaction_type": "withdrawal"
    },
    "timestamp": "2026-08-26T14:06:13+01:00"
}

    

Payout Webhook Payload

Field Description
event The webhook event name. For payout notifications this will be payout.finalized.
event_id Unique identifier generated for the webhook event.
status Final webhook status. Possible values are success or failed.
message Human-readable description of the final payout result.
data.reference The payout reference associated with the transaction.
data.amount Original payout transaction amount.
data.transaction_status Final payout transaction status. Possible values are SUCCESS or FAILED.
data.bank_name Destination bank name.
data.account_number Destination account number.
data.account_name Destination account holder name.
data.transaction_type Transaction type. For payouts this will normally be withdrawal.
timestamp ISO 8601 date and time when the webhook was generated.

Verify Payout Webhook Signature

MyBundlePay signs the exact JSON request body using HMAC SHA256 and the webhook secret configured on your account.

Generate the HMAC SHA256 signature from the raw request body and compare it with the value received in the X-Webhook-Signature header.

Do not generate the signature from decoded JSON. Always calculate it using the exact raw request body received from MyBundlePay.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

Route::post('/mybundlepay/webhook', function (Request $request) {

    /*
    |--------------------------------------------------------------------------
    | YOUR WEBHOOK SECRET
    |--------------------------------------------------------------------------
    */

    $webhookSecret =
        env('MYBUNDLEPAY_WEBHOOK_SECRET');


    /*
    |--------------------------------------------------------------------------
    | RAW REQUEST BODY
    |--------------------------------------------------------------------------
    */

    $rawPayload =
        $request->getContent();


    /*
    |--------------------------------------------------------------------------
    | SIGNATURE RECEIVED FROM MYBUNDLEPAY
    |--------------------------------------------------------------------------
    */

    $receivedSignature =
        $request->header(
            'X-Webhook-Signature'
        );


    /*
    |--------------------------------------------------------------------------
    | GENERATE EXPECTED SIGNATURE
    |--------------------------------------------------------------------------
    */

    $expectedSignature =
        hash_hmac(
            'sha256',
            $rawPayload,
            $webhookSecret
        );


    /*
    |--------------------------------------------------------------------------
    | VERIFY SIGNATURE
    |--------------------------------------------------------------------------
    */

    if (
        !$receivedSignature ||
        !hash_equals(
            $expectedSignature,
            $receivedSignature
        )
    ) {

        return response()->json([
            'status' => 'error',
            'message' => 'Invalid webhook signature.'
        ], 401);
    }


    /*
    |--------------------------------------------------------------------------
    | DECODE PAYLOAD
    |--------------------------------------------------------------------------
    */

    $payload =
        json_decode(
            $rawPayload,
            true
        );


    /*
    |--------------------------------------------------------------------------
    | HANDLE PAYOUT EVENT
    |--------------------------------------------------------------------------
    */

    if (
        ($payload['event'] ?? null)
        === 'payout.finalized'
    ) {

        $reference =
            $payload['data']['reference']
            ?? null;

        $status =
            $payload['status']
            ?? null;


        /*
        |--------------------------------------------------------------------------
        | SUCCESSFUL PAYOUT
        |--------------------------------------------------------------------------
        */

        if ($status === 'success') {

            // Mark your payout as successful.

        }


        /*
        |--------------------------------------------------------------------------
        | FAILED PAYOUT
        |--------------------------------------------------------------------------
        */

        if ($status === 'failed') {

            // Mark your payout as failed.

        }
    }


    /*
    |--------------------------------------------------------------------------
    | ACKNOWLEDGE WEBHOOK
    |--------------------------------------------------------------------------
    */

    return response()->json([
        'status' => 'success',
        'message' => 'Webhook received.'
    ], 200);

});

    

Expected Response

Your webhook endpoint should return HTTP status 200 OK after successfully receiving the notification.


{
    "status": "success",
    "message": "Webhook received."
}

    

Important Payout Webhook Notes

  • Always verify the X-Webhook-Signature before processing the webhook.
  • Use the data.reference field to locate the corresponding payout on your system.
  • Do not mark a payout successful simply because your API request was accepted. Wait for the final payout status or verify the transaction independently.
  • Your webhook endpoint should be idempotent. Receiving the same webhook more than once should not cause the transaction to be processed twice.
  • Store the event_id to help detect duplicate webhook events.
  • Respond with HTTP 200 OK as quickly as possible.

Virtual Account Funding Webhook (Dynamic Account)

When a customer successfully funds a Dynamic Virtual Account created through MyBundlePay, we will send a POST request to your configured webhook URL containing the payment details after the wallet has been credited successfully.

Webhook Headers

Content-Type: application/json

X-Webhook-Secret: Your configured webhook secret

X-Webhook-Signature: HMAC SHA256 signature generated using your webhook secret

Webhook Payload Example

Dynamic Virtual Account Funding Webhook

{
    "amount":"10000.00",
    "fee":"350.00",
    "netAmount":"9650.00",
    "accountName":"JOHN DOE",
    "bankName":"RUBIES",
    "method":"Virtual Account Funding",
    "externalReference":"MBP-0012-001",
    "paymentReference":"AO5SC4W914XLYFM3JPS1NUXLF8U7DR",
    "sessionId":"AO5SC4W914XLYFM3JPS1NUXLF8U7DR",
    "contractReference":"20260706FT00000000000000012586",
    "originatorAccountNumber":"1000001421",
    "creditAccount":"8880002186",
    "service":"INTERNAL", or "FUND_TRANSFER",
    "narration":"Wallet Funding",
    "status":"PAID",
    "date":"06 Jul 2026, 01:20 PM"
}

Merchant Customer Account Funding Webhook

{
    "eventId":"DEMO-EVENT-001",
    "event":"merchant_customer.account_funding",
    "amount":"10000.00",
    "fee":"520.00",
    "netAmount":"9480.00",
    "accountName":"DEMO CUSTOMER",
    "bankName":"DEMO BANK",
    "method":"Merchant Customer Account Funding",
    "externalReference":"DEMO-REF-001",
    "paymentReference":"DEMO-PAYMENT-001",
    "sessionId":"DEMO-SESSION-001",
    "contractReference":"DEMO-CONTRACT-001",
    "originatorAccountNumber":"0000000000",
    "originatorName":"DEMO SENDER",
    "creditAccount":"1111111111",
    "service":"INWARD",
    "narration":"DEMO MERCHANT CUSTOMER FUNDING",
    "status":"PAID",
    "date":"10 Aug 2026, 12:00 PM"
}

Webhook Payload Description

Field Description
amount Amount received from the customer.
fee Processing fee deducted by MyBundlePay.
netAmount Final amount credited to the merchant wallet.
accountName Name of the customer who made the payment.
bankName Sender's bank.
method Transaction method.
externalReference Your original reference supplied when creating the dynamic account.
paymentReference payment reference.
sessionId NIBSS Session ID.
contractReference contract reference.
originatorAccountNumber Customer's account number.
creditAccount The virtual account that received the payment.
service Transaction service type.
narration Payment narration.
status Transaction status.
date Date and time the webhook was generated.

Verify the Webhook

Every webhook request includes your configured webhook secret in the X-Webhook-Secret header and a request signature in the X-Webhook-Signature header. Verify both values before processing the payment.



Route::post('/webhook', function(Illuminate\Http\Request $request){

    $secret = env('MYBUNDLEPAY_WEBHOOK_SECRET');

    if($request->header('X-Webhook-Secret') !== $secret){

        return response()->json([
            "status"=>"error",
            "message"=>"Invalid webhook secret."
        ],401);

    }

    // Optional:
    // Verify X-Webhook-Signature here using HMAC SHA256.

    $payload = $request->all();

    // Process payment...

    return response()->json([
        "status"=>"success",
        "message"=>"Webhook received."
    ],200);

});


Expected Successful Response


{
    "status":"success",
    "message":"Webhook received."
}

Expected Failed Response


{
    "status":"error",
    "message":"Invalid webhook secret."
}

Best practices

Don't rely solely on webhooks

Have a backup strategy in place, in case your webhook endpoint fails. For instance, if your webhook endpoint is throwing server errors, you won't know about any new customer payments because webhook requests will fail.

Respond quickly

Your webhook endpoint needs to respond within a certain time limit, or we'll consider it a failure and try again. Avoid doing long-running tasks or network calls in your webhook endpoint so you don't hit the timeout. If your framework supports it, you can have your webhook endpoint immediately return a 200 status code, and then perform the rest of its duties; otherwise, you should dispatch any long-running tasks to a job queue, and then respond.