BursaPay Gateway API Docs
A REST JSON API for accepting payments. Two requests is all it takes to run a full payment flow.
Test vs Live modes
Use sk_test_... keys for test mode. Webhooks will NOT fire to localhost. Always call POST /payments/verify/ manually. Use sk_live_... keys for live mode where real NGN is charged and webhooks fire automatically.
✓ Unified Reference Format
All BursaPay transactions use a single reference format starting with BP-. Always use this reference to verify or query transactions.
Inline JS Modal SDK
Accept payments inline directly on your website using the bursapay.js overlay popup modal.
SDK CDN Script
Include script: <script src="https://bursapay.com/v1/bursapay.js"></script>
<!-- 1. Include BursaPay Inline SDK -->
<script src="https://bursapay.com/v1/bursapay.js"></script>
<!-- 2. Trigger popup modal -->
<script>
function payWithBursaPay() {
var popup = BursaPayPop.setup({
key: 'pk_live_your_publishable_key', // or pk_test_...
email: 'customer@example.com',
amount: 500000, // Amount in kobo / cents (₦5,000.00)
currency: 'NGN',
ref: 'BP-' + Math.floor(Math.random() * 1e9 + 1),
onSuccess: function(response) {
console.log('Payment successful! Reference:', response.reference);
// Handle success (e.g. redirect to receipt or call your backend)
},
onClose: function() {
console.log('Customer closed payment modal');
}
});
popup.openIframe();
}
</script>
<button onclick="payWithBursaPay()">Pay Now with BursaPay</button>
Authentication
Pass your secret key as a Bearer token in every request. Never expose secret keys on the frontend.
import requests
HEADERS = {"Authorization": "Bearer sk_test_your_key_here", "Content-Type": "application/json"}
BASE = "https://api.bursapay.com/api/v1"
resp = requests.post(f"{BASE}/payments/initialize/", json={"amount": 5000, "email": "john@example.com"}, headers=HEADERS)
print(resp.json())
const res = await fetch('https://api.bursapay.com/api/v1/payments/initialize/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_your_key_here', 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 5000, email: 'john@example.com' }),
});
const data = await res.json();
console.log(data);
const axios = require('axios');
const { data } = await axios.post(
'https://api.bursapay.com/api/v1/payments/initialize/',
{ amount: 5000, email: 'john@example.com' },
{ headers: { Authorization: 'Bearer sk_test_your_key_here' } }
);
console.log(data);
$ch = curl_init('https://api.bursapay.com/api/v1/payments/initialize/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['amount' => 5000, 'email' => 'john@example.com']),
CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_test_your_key_here', 'Content-Type: application/json']
]);
print_r(json_decode(curl_exec($ch), true));
const headers: Record<string, string> = {
Authorization: 'Bearer sk_test_your_key_here',
'Content-Type': 'application/json',
};
const res = await fetch('https://api.bursapay.com/api/v1/payments/initialize/', {
method: 'POST', headers, body: JSON.stringify({ amount: 5000, email: 'john@example.com' })
});
const data = await res.json();
curl -X POST https://api.bursapay.com/api/v1/payments/initialize/ \
-H "Authorization: Bearer sk_test_your_key_here" \
-H "Content-Type: application/json" \
-d '{"amount":5000,"email":"john@example.com"}'
Payment Flow
- Initialize Payment: Call
POST /payments/initialize/. Store theBP-...reference. - Redirect: Redirect customer to
authorization_url. - Callback: Customer returns to your
callback_urlwith reference. - Verify: Call
POST /payments/verify/with the reference. - Webhook: Wait for webhook (live mode only).
Initialize Payment
POST payments/initialize/
Creates a payment session and returns an authorization_url.
⚠ Save the reference before redirecting
The response contains data.reference. Store this in your database immediately to verify later.
| Parameter | Type | Description |
|---|---|---|
amountrequired | number | Amount in NGN. Minimum: 100. |
emailrequired | string | Customer's email address. |
currencyoptional | string | ISO 4217 code. Defaults to NGN. |
callback_urloptional | string | URL to redirect after payment. |
metadataoptional | object | Any custom data to attach. |
idempotency_keyoptional | string | Prevents duplicate charges. |
splitsoptional | array | Marketplace split payment configuration. |
import requests
resp = requests.post(
"https://api.bursapay.com/api/v1/payments/initialize/",
json={"amount": 5000, "email": "john.doe@example.com"},
headers={"Authorization": "Bearer sk_test_xxxx"}
)
print(resp.json()["data"]["authorization_url"])
curl -X POST https://api.bursapay.com/api/v1/payments/initialize/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"amount":5000,"email":"john.doe@example.com"}'
{
"status": "success",
"authorization_url": "https://api.bursapay.com/pay/BP-xxx",
"access_code": "0peioxfhpn",
"reference": "BP-xxx"
}
Verify Payment
POST payments/verify/
Confirms a payment status. Idempotent — safe to call repeatedly.
✓ Pass the BursaPay Reference
Pass the exact BursaPay reference (BP-...) to confirm.
resp = requests.post("https://api.bursapay.com/api/v1/payments/verify/", json={"reference": "BP-xxx"}, headers={"Authorization": "Bearer sk_test_xxxx"})
print(resp.json())
curl -X POST https://api.bursapay.com/api/v1/payments/verify/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"reference":"BP-xxx"}'
Get Payment
Retrieve a payment by reference.
GET /payments/{reference}/
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
response = requests.get("https://api.bursapay.com/api/v1/payments/BP-xxx/", headers=headers)
print(response.json())
fetch('https://api.bursapay.com/api/v1/payments/BP-xxx/', {
headers: { 'Authorization': 'Bearer sk_test_xxxx' }
})
.then(res => res.json())
.then(console.log);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.bursapay.com/api/v1/payments/BP-xxx/");
curl_setopt($ch, CURLOPT_HTTPHEADER, array("Authorization: Bearer sk_test_xxxx"));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
echo $result;
List Payments
Paginated list with status, date, and query filters.
GET /payments/
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number |
per_page | integer | Records per page |
status | string | Payment status |
from | date | Start date |
to | date | End date |
q | string | Search query |
Payment Intents & 2-Step Authorize/Capture
Enables 2-step payment authorization hold and delayed capture for e-commerce order fulfillment, pre-orders, and rentals.
1. Create Payment Intent
POST payment-intents/
Creates a payment intent with a 7-day authorization hold (capture_method: "manual").
import requests
resp = requests.post(
"https://api.bursapay.com/api/v1/payment-intents/",
json={
"amount": 15000,
"email": "customer@example.com",
"currency": "NGN",
"capture_method": "manual",
"metadata": {"order_id": "ORD-88219"}
},
headers={"Authorization": "Bearer sk_test_xxxx"}
)
print(resp.json()["data"]["intent_reference"])
curl -X POST https://api.bursapay.com/api/v1/payment-intents/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"amount":15000,"email":"customer@example.com","capture_method":"manual"}'
const response = await fetch('https://api.bursapay.com/api/v1/payment-intents/', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_xxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: 15000,
email: 'customer@example.com',
capture_method: 'manual'
})
});
const data = await response.json();
console.log(data);
2. Capture Authorized Intent
POST payment-intents/{intent_reference}/capture/
Captures authorized funds upon order fulfillment and credits your wallet.
curl -X POST https://api.bursapay.com/api/v1/payment-intents/PI-3M8V182A/capture/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"amount_to_capture": 15000}'
resp = requests.post(
"https://api.bursapay.com/api/v1/payment-intents/PI-3M8V182A/capture/",
json={"amount_to_capture": 15000},
headers={"Authorization": "Bearer sk_test_xxxx"}
)
print(resp.json()["data"]["status"]) # succeeded
3. Cancel Authorization Hold
POST payment-intents/{intent_reference}/cancel/
Releases authorization hold if order is canceled before fulfillment.
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
params = {"page": 1, "per_page": 20, "status": "success"}
response = requests.get("https://api.bursapay.com/api/v1/payments/", headers=headers, params=params)
print(response.json())
fetch('https://api.bursapay.com/api/v1/payments/?page=1&per_page=20&status=success', {
headers: { 'Authorization': 'Bearer sk_test_xxxx' }
})
.then(res => res.json())
.then(console.log);
curl -X GET 'https://api.bursapay.com/api/v1/payments/?page=1&per_page=20&status=success' \
-H "Authorization: Bearer sk_test_xxxx"
{
"status": "success",
"data": [
{ "reference": "BP-xxx", "amount": 5000, "status": "success" }
],
"meta": { "total": 1, "page": 1, "per_page": 20 }
}
Inline Charge
Charge saved card with authorization_code.
POST /payments/charge/
| Parameter | Type | Description |
|---|---|---|
customer_reference * | string | Customer reference |
amount * | integer | Amount |
authorization_code * | string | Authorization code |
metadata | object | Additional metadata |
| Error Code | Status Code | Description |
|---|---|---|
customer_not_found | 404 | Customer not found |
charge_failed | 400 | Charge failed |
gateway_timeout | 504 | Gateway timeout |
provider_error | 502 | Provider error |
curl -X POST https://api.bursapay.com/api/v1/payments/charge/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"customer_reference":"CUST-123","amount":5000,"authorization_code":"AUTH-xyz"}'
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
data = {"customer_reference": "CUST-123", "amount": 5000, "authorization_code": "AUTH-xyz"}
response = requests.post("https://api.bursapay.com/api/v1/payments/charge/", headers=headers, json=data)
print(response.json())
fetch('https://api.bursapay.com/api/v1/payments/charge/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_xxxx', 'Content-Type': 'application/json' },
body: JSON.stringify({ customer_reference: "CUST-123", amount: 5000, authorization_code: "AUTH-xyz" })
})
.then(res => res.json())
.then(console.log);
Bulk Payments
Process up to 50 payments in a single request. ≤10 payments are processed synchronously (200 OK), >10 payments are processed asynchronously (202 Accepted).
POST /payments/bulk/
| Parameter | Type | Description |
|---|---|---|
payments * | array | Array of payment objects |
payments[].amount | integer | Amount |
payments[].customer_reference | string | Customer reference |
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
data = {"payments": [{"amount": 1000, "customer_reference": "CUST-1"}, {"amount": 2000, "customer_reference": "CUST-2"}]}
response = requests.post("https://api.bursapay.com/api/v1/payments/bulk/", headers=headers, json=data)
print(response.json())
const fetch = require('node-fetch');
fetch('https://api.bursapay.com/api/v1/payments/bulk/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_xxxx', 'Content-Type': 'application/json' },
body: JSON.stringify({ payments: [{ amount: 1000, customer_reference: "CUST-1" }] })
})
.then(res => res.json())
.then(console.log);
curl -X POST https://api.bursapay.com/api/v1/payments/bulk/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"payments":[{"amount":1000,"customer_reference":"CUST-1"}]}'
Sync Response (200 OK):
{
"status": "success",
"data": { "processed": 2, "failed": 0 }
}
Async Response (202 Accepted):
{
"status": "processing",
"batch_reference": "BATCH-123",
"message": "Batch processing started"
}
To check batch status: GET /payments/bulk/<batch_reference>/
Customers
Manage customers.
POST /customers/ and GET /customers/
| Parameter | Type | Description |
|---|---|---|
email * | string | Customer email |
name | string | Customer name |
phone | string | Customer phone |
metadata | object | Additional metadata |
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
data = {"email": "test@example.com", "name": "Test User"}
response = requests.post("https://api.bursapay.com/api/v1/customers/", headers=headers, json=data)
print(response.json())
fetch('https://api.bursapay.com/api/v1/customers/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_xxxx', 'Content-Type': 'application/json' },
body: JSON.stringify({ email: "test@example.com", name: "Test User" })
})
.then(res => res.json())
.then(console.log);
{
"status": "success",
"data": {
"reference": "CUST-abc",
"email": "test@example.com",
"name": "Test User"
}
}
Customer Detail
Retrieve customer with recent payments.
GET /customers/{reference}/
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
response = requests.get("https://api.bursapay.com/api/v1/customers/CUST-abc/", headers=headers)
print(response.json())
fetch('https://api.bursapay.com/api/v1/customers/CUST-abc/', {
headers: { 'Authorization': 'Bearer sk_test_xxxx' }
})
.then(res => res.json())
.then(console.log);
Bulk Customer Import
Upload a UTF-8 CSV file to create or update customers in bulk. Rows are processed independently — failures are reported per-row without stopping the import.
POST /customers/import/
Use Content-Type: multipart/form-data with a field named file. Maximum file size: 5 MB.
CSV Format
| Column | Required | Description |
|---|---|---|
email | Yes | Customer email — used as the unique identifier for upserts |
name | No | Full name of the customer |
phone | No | Phone number |
metadata | No | JSON object string (e.g. {"plan":"gold"}) |
Request
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
with open("customers.csv", "rb") as f:
response = requests.post(
"https://api.bursapay.com/api/v1/customers/import/",
headers=headers,
files={"file": ("customers.csv", f, "text/csv")},
)
print(response.json())
curl -X POST https://api.bursapay.com/api/v1/customers/import/ \
-H "Authorization: Bearer sk_test_xxxx" \
-F "file=@customers.csv;type=text/csv"
const form = new FormData();
form.append('file', csvBlob, 'customers.csv');
fetch('https://api.bursapay.com/api/v1/customers/import/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_xxxx' },
body: form,
})
.then(res => res.json())
.then(console.log);
Response
{
"success": true,
"data": {
"created": 42,
"updated": 8,
"failed": 2,
"errors": [
{ "row": 5, "reason": "Invalid email address: 'not-an-email'" },
{ "row": 11, "reason": "metadata is not valid JSON: '{bad}'" }
]
}
}
Error Codes
| Code | HTTP | Description |
|---|---|---|
validation_error | 400 | File field missing, empty, or CSV has no email header |
file_too_large | 400 | Uploaded file exceeds the 5 MB limit |
insufficient_scope | 403 | API key is missing the customers:write scope |
Wallet Balance
Retrieve wallet balance and ledger history.
GET /wallet/balance
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
response = requests.get("https://api.bursapay.com/api/v1/wallet/balance/", headers=headers)
print(response.json())
curl -X GET https://api.bursapay.com/api/v1/wallet/balance/ \
-H "Authorization: Bearer sk_test_xxxx"
{
"status": "success",
"data": {
"available_balance": 50000,
"pending_balance": 2000,
"total_credits": 150000,
"total_debits": 100000,
"currency": "NGN"
}
}
Wallet Ledger
GET /wallet/ledger/ with cursor pagination.
| Parameter | Type | Description |
|---|---|---|
entry_type | string | Filter by entry type |
per_page | integer | Records per page |
cursor | string | Pagination cursor |
from | date | Start date |
to | date | End date |
{
"status": "success",
"data": [
{
"id": "ledg_xyz",
"amount": 5000,
"balance_before": 45000,
"balance_after": 50000
}
]
}
Refunds
Initiate a full or partial refund.
POST /refunds/
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
data = {"payment_reference": "BP-xxx", "amount": 1000}
response = requests.post("https://api.bursapay.com/api/v1/refunds/", headers=headers, json=data)
print(response.json())
const fetch = require('node-fetch');
fetch('https://api.bursapay.com/api/v1/refunds/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_xxxx', 'Content-Type': 'application/json' },
body: JSON.stringify({ payment_reference: "BP-xxx", amount: 1000 })
})
.then(res => res.json())
.then(console.log);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.bursapay.com/api/v1/refunds/");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["payment_reference" => "BP-xxx", "amount" => 1000]));
curl_setopt($ch, CURLOPT_HTTPHEADER, array("Authorization: Bearer sk_test_xxxx", "Content-Type: application/json"));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
echo $result;
Refund Detail
Retrieve refund status and details.
GET /refunds/{refund_reference}/
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
response = requests.get("https://api.bursapay.com/api/v1/refunds/REF-xyz/", headers=headers)
print(response.json())
fetch('https://api.bursapay.com/api/v1/refunds/REF-xyz/', {
headers: { 'Authorization': 'Bearer sk_test_xxxx' }
})
.then(res => res.json())
.then(console.log);
curl -X GET https://api.bursapay.com/api/v1/refunds/REF-xyz/ \
-H "Authorization: Bearer sk_test_xxxx"
{
"status": "success",
"data": {
"refund_reference": "REF-xyz",
"payment_reference": "BP-xxx",
"amount": 1000,
"reason": "customer requested",
"status": "completed"
}
}
Webhooks
Verify webhook events using HMAC signature.
| Event | Description |
|---|---|
payment.success | Fired when a payment succeeds |
payment.failed | Fired when a payment fails |
refund.completed | Fired when a refund completes |
transfer.success | Fired when a transfer succeeds |
transfer.failed | Fired when a transfer fails |
subscription.charged | Fired when a subscription charge succeeds |
subscription.charge_failed | Fired when a subscription charge fails |
subscription.cancelled | Fired when a subscription is cancelled |
virtual_account.credited | Fired when a virtual account receives funds |
dispute.created | Fired when a new dispute is created |
dispute.won | Fired when a dispute is resolved in your favour |
dispute.lost | Fired when a dispute is resolved against you |
withdrawal.completed | Fired when a withdrawal completes |
customer.created | Fired when a customer is created |
Payload versioning: Webhooks contain version and sent_at fields for security and replay protection.
import hmac
import hashlib
def verify_webhook(payload, signature, secret):
expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha512).hexdigest()
return expected == signature
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const hash = crypto.createHmac('sha512', secret).update(payload).digest('hex');
return hash === signature;
}
function verifyWebhook($payload, $signature, $secret) {
$expected = hash_hmac('sha512', $payload, $secret);
return hash_equals($expected, $signature);
}
import (
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
)
func VerifyWebhook(payload []byte, signature, secret string) bool {
h := hmac.New(sha512.New, []byte(secret))
h.Write(payload)
expected := hex.EncodeToString(h.Sum(nil))
return expected == signature
}
require 'openssl'
def verify_webhook(payload, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA512', secret, payload)
expected == signature
end
import * as crypto from 'crypto';
export function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const hash = crypto.createHmac('sha512', secret).update(payload).digest('hex');
return hash === signature;
}
Webhook Management
Manage webhook endpoints programmatically.
| Endpoint | Method | Description |
|---|---|---|
/webhooks/ | POST | Create a new webhook |
/webhooks/ | GET | List all webhooks |
/webhooks/{id}/ | PATCH | Update a webhook |
/webhooks/{id}/ | DELETE | Delete a webhook |
import requests
headers = {"Authorization": "Bearer sk_test_xxxx"}
data = {"url": "https://myapp.com/webhook", "events": ["payment.success"]}
response = requests.post("https://api.bursapay.com/api/v1/webhooks/", headers=headers, json=data)
print(response.json())
fetch('https://api.bursapay.com/api/v1/webhooks/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_xxxx', 'Content-Type': 'application/json' },
body: JSON.stringify({ url: "https://myapp.com/webhook", events: ["payment.success"] })
})
.then(res => res.json())
.then(console.log);
curl -X POST https://api.bursapay.com/api/v1/webhooks/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://myapp.com/webhook","events":["payment.success"]}'
Webhook Event Catalog
GET /webhooks/events/
Returns the full list of webhook event types. No authentication required. Events marked Coming Soon are on the roadmap and will be activated as features ship.
| Event | Description | Status |
|---|---|---|
payment.success | A payment was completed successfully | AVAILABLE |
payment.failed | A payment attempt failed | AVAILABLE |
refund.initiated | A refund was initiated | AVAILABLE |
refund.completed | A refund completed successfully | AVAILABLE |
refund.failed | A refund failed | AVAILABLE |
transfer.success | A payout transfer completed successfully | AVAILABLE |
transfer.failed | A payout transfer failed | AVAILABLE |
subscription.charged | A subscription charge succeeded | AVAILABLE |
subscription.charge_failed | A subscription charge failed | AVAILABLE |
subscription.cancelled | A subscription was cancelled | AVAILABLE |
virtual_account.credited | A virtual account received funds | AVAILABLE |
dispute.created | A new payment dispute was created | AVAILABLE |
dispute.won | A dispute was resolved in your favour | AVAILABLE |
dispute.lost | A dispute was resolved against you | AVAILABLE |
api_key.rotated | An API key was rotated | AVAILABLE |
webhook.test | Manual test event | AVAILABLE |
subscription.created | A subscription was created | COMING SOON |
{
"success": true,
"data": {
"events": [
{ "event": "payment.success", "description": "A payment was completed successfully", "available": true },
{ "event": "payment.failed", "description": "A payment attempt failed", "available": true },
{ "event": "transfer.success","description": "A payout transfer completed", "available": true }
]
}
}
Webhook Log Detail
GET /webhooks/logs/<log_id>/
Retrieve a single webhook delivery log with full payload and response body. Use this to debug failed deliveries — the list view omits these fields to keep responses lightweight.
| Path Parameter | Type | Description |
|---|---|---|
log_id required | integer | The ID of the webhook delivery log (from the logs list) |
{
"success": true,
"data": {
"id": 42,
"event_type": "payment.success",
"status": "failed",
"response_code": 500,
"delivery_duration_ms": 1203,
"retry_count": 2,
"delivered_at": null,
"next_retry_at": "2026-07-24T12:35:00Z",
"created_at": "2026-07-24T12:30:00Z",
"payload": {
"event": "payment.success",
"version": "2026-07-01",
"data": { "reference": "BP-9ECF1DCF148E446B", "amount": "5000.00", "status": "success" }
},
"response_body": "Internal Server Error"
}
}
Health Check
Check API availability. No auth required.
GET /health/
curl -X GET https://api.bursapay.com/api/v1/health/
import requests
response = requests.get("https://api.bursapay.com/api/v1/health/")
print(response.json())
Transfers
⚠ Live Mode Required
Transfers are live mode only. Use an sk_live_ key. Test keys return 403 transfer_not_allowed.
Send funds from your developer wallet to any Nigerian bank account. Transfers are processed asynchronously — initial status is processing. Listen for transfer.success or transfer.failed webhook events.
POST /transfers/
| Parameter | Type | Description |
|---|---|---|
amount required | number | Amount in NGN. Minimum ₦100. |
bank_code required | string | Standard bank code (e.g. "058" for GTBank). Call GET /transfers/banks/ for supported codes. |
account_number required | string | 10-digit NUBAN account number. |
account_name required | string | Account holder name as registered with the bank. |
narration optional | string | Description on recipient's bank statement. Max 255 chars. |
metadata optional | object | Arbitrary key-value data (e.g. vendor ID, order reference). |
import requests
resp = requests.post(
"https://api.bursapay.com/api/v1/transfers/",
json={
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"account_name": "Chidi Okeke",
"narration": "Vendor payment — Invoice #1042",
"metadata": {"vendor_id": "VND-001"}
},
headers={
"Authorization": "Bearer sk_live_xxxx",
"Content-Type": "application/json",
"Idempotency-Key": "unique-key-per-transfer-001"
}
)
print(resp.json())
const { data } = await axios.post(
'https://api.bursapay.com/api/v1/transfers/',
{
amount: 5000, bank_code: '058',
account_number: '0123456789', account_name: 'Chidi Okeke',
narration: 'Vendor payment — Invoice #1042',
},
{
headers: {
Authorization: 'Bearer sk_live_xxxx',
'Idempotency-Key': 'unique-key-per-transfer-001'
}
}
);
curl -X POST https://api.bursapay.com/api/v1/transfers/ \
-H "Authorization: Bearer sk_live_xxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-transfer-key-001" \
-d '{"amount":5000,"bank_code":"058","account_number":"0123456789","account_name":"Chidi Okeke"}'
{
"success": true,
"data": {
"reference": "TRF-A1B2C3D4E5F60001",
"amount": "5000.00",
"bank_code": "058",
"account_number": "0123456789",
"account_name": "Chidi Okeke",
"narration": "Vendor payment — Invoice #1042",
"status": "processing",
"created_at": "2026-07-01T14:32:00.000000+00:00",
"completed_at": null
}
}
| HTTP | Code | When |
|---|---|---|
| 403 | transfer_not_allowed | Test key used. Switch to sk_live_. |
| 400 | insufficient_wallet_balance | Wallet balance below requested amount. |
| 400 | validation_error | Invalid field (non-numeric account, amount < 100). |
| 502 | provider_error | Network rejected (invalid bank code or account). |
List & Get Transfers
GET /transfers/ |
GET /transfers/<reference>/
H = {"Authorization": "Bearer sk_live_xxxx"}
# List
transfers = requests.get("https://api.bursapay.com/api/v1/transfers/", headers=H).json()
# Detail
transfer = requests.get("https://api.bursapay.com/api/v1/transfers/TRF-xxxx/", headers=H).json()
curl https://api.bursapay.com/api/v1/transfers/ -H "Authorization: Bearer sk_live_xxxx"
curl https://api.bursapay.com/api/v1/transfers/TRF-xxxx/ -H "Authorization: Bearer sk_live_xxxx"
Transfer Webhooks: listen for transfer.success and transfer.failed events to track settlement.
Payment Links
Create shareable payment links for invoices, tickets, donations, and onboarding. No redirect required — customers pay via a hosted checkout page.
POST /payment-links/
| Parameter | Type | Description |
|---|---|---|
amount required | number | Amount to charge in NGN |
title required | string | Title shown on checkout page |
description optional | string | Additional description |
expires_at optional | datetime | Expiration (ISO 8601) |
metadata optional | object | Custom key-value data |
resp = requests.post(
"https://api.bursapay.com/api/v1/payment-links/",
json={"amount": 180000, "title": "Invoice #1042", "expires_at": "2025-12-31T23:59:59Z"},
headers={"Authorization": "Bearer sk_test_xxxx"}
)
link = resp.json()["data"]
print(link["checkout_url"]) # share this URL with the customer
const { data } = await (await fetch('https://api.bursapay.com/api/v1/payment-links/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_xxxx', 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 180000, title: 'Invoice #1042' })
})).json();
console.log(data.checkout_url);
curl -X POST https://api.bursapay.com/api/v1/payment-links/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"amount":180000,"title":"Invoice #1042"}'
{
"success": true,
"data": {
"link_code": "PLNK-abc123",
"title": "Invoice #1042",
"amount": "180000.00",
"checkout_url": "https://checkout.bursapay.com/pay/PLNK-abc123",
"is_active": true,
"created_at": "2026-07-24T10:00:00Z"
}
}
GET /payment-links/ |
GET /payment-links/{code}/ |
PATCH /payment-links/{code}/ |
DELETE /payment-links/{code}/
Public checkout URL format: https://checkout.bursapay.com/pay/{code}
Bulk Payment Link Creation
Create up to 50 payment links in a single atomic request. All items are validated before any links are created — if one item fails, zero links are created (all-or-nothing).
POST /payment-links/bulk/
Rate limit: 10 requests per hour per API key.
| Parameter | Type | Description |
|---|---|---|
| Request body | array | JSON array of up to 50 payment link objects. Each item uses the same schema as POST /payment-links/. Max array length: 50. |
title required | string | Title shown on checkout page (max 255 chars) |
amount optional | number | Amount in the specified currency. Null = customer sets amount. |
currency optional | string | 3-letter ISO currency code (default: NGN) |
description optional | string | Additional description |
max_uses optional | integer | Maximum number of uses. Null = unlimited. |
expires_at optional | datetime | Expiration datetime (ISO 8601, must be in the future) |
success_url optional | string | Redirect URL on successful payment |
cancel_url optional | string | Redirect URL on cancelled payment |
metadata optional | object | Custom key-value data |
resp = requests.post(
"https://api.bursapay.com/api/v1/payment-links/bulk/",
json=[
{"title": "Invoice #1001", "amount": 50000},
{"title": "Invoice #1002", "amount": 75000, "expires_at": "2025-12-31T23:59:59Z"},
{"title": "Event Ticket", "amount": 10000, "max_uses": 100},
],
headers={"Authorization": "Bearer sk_test_xxxx"}
)
links = resp.json()["data"]
for link in links:
print(link["link_code"], link["checkout_url"])
const { data } = await (await fetch('https://api.bursapay.com/api/v1/payment-links/bulk/', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_test_xxxx', 'Content-Type': 'application/json' },
body: JSON.stringify([
{ title: 'Invoice #1001', amount: 50000 },
{ title: 'Invoice #1002', amount: 75000 },
])
})).json();
data.forEach(link => console.log(link.link_code, link.checkout_url));
curl -X POST https://api.bursapay.com/api/v1/payment-links/bulk/ \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '[{"title":"Invoice #1001","amount":50000},{"title":"Invoice #1002","amount":75000}]'
// 201 Created — all links created
{
"success": true,
"data": [
{
"link_code": "pl_AbCdEfGhIjKlMn",
"title": "Invoice #1001",
"amount": "50000.00",
"currency": "NGN",
"checkout_url": "https://bursapay.com/direct-pay/pl_AbCdEfGhIjKlMn/",
"is_active": true,
"created_at": "2026-07-24T10:00:00Z"
},
{ "...": "..." }
]
}
// 400 Bad Request — per-item validation errors (zero links created)
{
"success": false,
"code": "validation_error",
"message": "One or more items failed validation.",
"errors": {
"items": {
"1": { "expires_at": ["Must be a future datetime."] },
"3": { "title": ["This field is required."] }
}
}
}
| Status | Condition |
|---|---|
201 | All items valid; links created atomically |
400 | Non-array body, empty array, >50 items, or any item fails validation |
429 | Rate limit exceeded (10 requests/hour) |
Developer Analytics
Query your transaction metrics programmatically. Get volume, success rates, channel breakdowns, and daily trends for any time window — no portal scraping needed.
GET /analytics/
Requires a secret key (sk_live or sk_test). Rate limit: 60 requests per hour.
| Parameter | Type | Description |
|---|---|---|
period optional | string | today, 7d, 30d (default), 90d |
currency optional | string | Currency code, e.g. NGN (default), USD, GBP, KES |
resp = requests.get(
"https://api.bursapay.com/api/v1/analytics/",
params={"period": "30d", "currency": "NGN"},
headers={"Authorization": "Bearer sk_live_xxxx"}
)
data = resp.json()["data"]
print(f"Volume: {data['total_volume']} Transactions: {data['transaction_count']}")
print(f"Success rate: {data['success_rate']}%")
const res = await fetch('https://api.bursapay.com/api/v1/analytics/?period=30d¤cy=NGN', {
headers: { 'Authorization': 'Bearer sk_live_xxxx' }
});
const { data } = await res.json();
console.log(`Volume: ${data.total_volume}, Transactions: ${data.transaction_count}`);
curl "https://api.bursapay.com/api/v1/analytics/?period=30d¤cy=NGN" \
-H "Authorization: Bearer sk_live_xxxx"
{
"success": true,
"data": {
"period": "30d",
"currency": "NGN",
"total_volume": "125000.00",
"transaction_count": 47,
"success_rate": 93.5,
"average_transaction_value": "2659.57",
"top_channels": [
{"channel": "card", "count": 30, "volume": "95000.00"},
{"channel": "bank_transfer", "count": 17, "volume": "30000.00"}
],
"daily_breakdown": [
{"date": "2025-01-01", "volume": "5000.00", "count": 2},
{"date": "2025-01-02", "volume": "12500.00", "count": 5}
]
}
}
Period windows
today = current day | 7d = last 7 days | 30d = last 30 days | 90d = last 90 days. All ranges are inclusive of both endpoints.
Payment Fulfillment
Attach delivery proof to any payment so BursaPay can automatically populate dispute evidence if a chargeback is raised. All fields are optional — supply any combination you have available.
GET /payments/{reference}/fulfillment/ Retrieve fulfillment data
PUT /payments/{reference}/fulfillment/ Create or replace fulfillment data
Returns 404 when the payment is not found or belongs to a different developer — preventing reference enumeration across accounts.
| Field | Type | Description |
|---|---|---|
delivery_url optional | URL string | Link to delivery confirmation page or asset |
delivery_confirmed_at optional | ISO 8601 string | Datetime the delivery was confirmed, e.g. 2025-01-15T10:30:00Z |
service_description optional | string | Description of goods or services delivered (max 1000 chars) |
proof_of_delivery optional | string | Proof narrative, audit log, or signed acknowledgment (max 5000 chars) |
import requests
BASE = "https://api.bursapay.com/api/v1"
HEADERS = {"Authorization": "Bearer sk_live_xxxx", "Content-Type": "application/json"}
# Attach fulfillment proof
resp = requests.put(
f"{BASE}/payments/BP-abc123/fulfillment/",
json={
"delivery_url": "https://example.com/delivery/receipt-456",
"delivery_confirmed_at": "2025-01-15T10:30:00Z",
"service_description": "SaaS subscription — annual plan",
"proof_of_delivery": "Customer activated on 2025-01-15, confirmed via email audit log."
},
headers=HEADERS
)
print(resp.json())
# Retrieve fulfillment proof
resp = requests.get(f"{BASE}/payments/BP-abc123/fulfillment/", headers=HEADERS)
print(resp.json()["data"])
const BASE = 'https://api.bursapay.com/api/v1';
const headers = { 'Authorization': 'Bearer sk_live_xxxx', 'Content-Type': 'application/json' };
// Attach fulfillment proof
const res = await fetch(`${BASE}/payments/BP-abc123/fulfillment/`, {
method: 'PUT',
headers,
body: JSON.stringify({
delivery_url: 'https://example.com/delivery/receipt-456',
delivery_confirmed_at: '2025-01-15T10:30:00Z',
service_description: 'SaaS subscription — annual plan',
proof_of_delivery: 'Customer activated on 2025-01-15.'
})
});
const data = await res.json();
console.log(data);
curl -X PUT "https://api.bursapay.com/api/v1/payments/BP-abc123/fulfillment/" \
-H "Authorization: Bearer sk_live_xxxx" \
-H "Content-Type: application/json" \
-d '{
"delivery_url": "https://example.com/delivery/receipt-456",
"delivery_confirmed_at": "2025-01-15T10:30:00Z",
"service_description": "SaaS subscription — annual plan",
"proof_of_delivery": "Customer activated on 2025-01-15."
}'
{
"success": true,
"data": {
"delivery_url": "https://example.com/delivery/receipt-456",
"delivery_confirmed_at": "2025-01-15T10:30:00Z",
"service_description": "SaaS subscription — annual plan",
"proof_of_delivery": "Customer activated on 2025-01-15, confirmed via email audit log."
}
}
Chargeback pre-emption
When a dispute.created event arrives from Paystack, BursaPay automatically copies this fulfillment data into the dispute's pre-filled evidence so you don't have to re-submit it manually during the dispute window.
Sandbox Seeding API
Instantly populate your test environment with synthetic data so you can demo the portal, run integration tests, or explore analytics without setting up records manually.
⚠ Test mode only
Both endpoints require a sk_test_ key and will return 403 live_mode_not_allowed if called with a live key.
POST sandbox/seed/
Creates a set of synthetic payments, customers, and related records for the chosen scenario. Returns a count of each record type created.
| Body field | Type | Required | Description |
|---|---|---|---|
scenario | string | required | One of basic, high_volume, disputes, subscriptions |
| Scenario | Records created |
|---|---|
basic | 10 payments, 5 customers |
high_volume | 100 payments, 20 customers |
disputes | 5 payments, 5 customers, 2 disputes |
subscriptions | 3 plans, 10 subscriptions, 10 customers |
24-hour idempotency
Calling POST sandbox/seed/ a second time with the same scenario within 24 hours returns 429. Delete the seed data first (or wait for the TTL to expire) to re-seed.
import requests
resp = requests.post(
"https://api.bursapay.com/api/v1/sandbox/seed/",
json={"scenario": "basic"},
headers={"Authorization": "Bearer sk_test_xxxx"}
)
print(resp.json())
# {"success": true, "data": {"created": {"payments": 10, "customers": 5}}}
const res = await fetch('https://api.bursapay.com/api/v1/sandbox/seed/', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_xxxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({ scenario: 'high_volume' })
});
const data = await res.json();
console.log(data.data.created); // { payments: 100, customers: 20 }
curl -X POST "https://api.bursapay.com/api/v1/sandbox/seed/" \
-H "Authorization: Bearer sk_test_xxxx" \
-H "Content-Type: application/json" \
-d '{"scenario": "disputes"}'
// 201 Created
{
"success": true,
"data": {
"created": {
"payments": 5,
"customers": 5,
"disputes": 2
}
}
}
DELETE sandbox/seed/
Deletes all records that were created by a previous seed call — payments, customers, disputes, subscriptions, and plans. The 24-hour idempotency lock is also cleared, so you can re-seed immediately.
resp = requests.delete(
"https://api.bursapay.com/api/v1/sandbox/seed/",
headers={"Authorization": "Bearer sk_test_xxxx"}
)
print(resp.json())
# {"success": true, "data": {"deleted": 17}}
const res = await fetch('https://api.bursapay.com/api/v1/sandbox/seed/', {
method: 'DELETE',
headers: { 'Authorization': 'Bearer sk_test_xxxx' }
});
const data = await res.json();
console.log(`Deleted ${data.data.deleted} records`);
curl -X DELETE "https://api.bursapay.com/api/v1/sandbox/seed/" \
-H "Authorization: Bearer sk_test_xxxx"
// 200 OK
{
"success": true,
"data": {
"deleted": 17
}
}
| Status | Code | When |
|---|---|---|
400 | invalid_scenario | Missing or unrecognised scenario value; response includes errors.valid_scenarios list |
403 | live_mode_not_allowed | Request was made with a live key (sk_live_) |
429 | seed_already_exists | Same scenario was seeded within the last 24 hours; delete first or wait |
500 | seed_failed | DB write failed; transaction was rolled back, no records created |
Live Playground
Test the API directly. Paste your sk_test_ key, fill in the fields, and hit Initialize. A real request will be made — no real money moves in test mode.
Errors & Limits
Every error response uses the same JSON shape. The code field is machine-readable so you can switch on it without parsing the message string.
{
"success": false,
"code": "validation_error",
"message": "Validation failed.",
"errors": { "amount": ["This field is required."] },
"request_id": "req_a1b2c3d4e5f6g7h8"
}
HTTP Status Codes
| Status | Meaning | When |
|---|---|---|
200 | OK | Request succeeded. |
201 | Created | New resource created. |
400 | Bad Request | Missing/invalid fields or business rule violated. |
401 | Unauthorized | No API key, invalid key, or key deactivated. |
403 | Forbidden | Live mode not enabled, key/mode mismatch, or IP blocked. |
404 | Not Found | Payment, customer, or resource reference does not exist. |
409 | Conflict | Idempotency key already used for a different payload. |
415 | Unsupported Media | Content-Type must be application/json. |
422 | Unprocessable | Valid request but semantically impossible (e.g. refund exceeds original). |
429 | Too Many Requests | Rate limit hit. Check the Retry-After response header. |
500 | Internal Error | Unexpected server error. Log request_id and contact support. |
502 | Provider Error | Payment network returned unexpected error. Retry after a delay. |
504 | Gateway Timeout | Payment network timed out. Safe to retry. |
Common Error Codes
| code | HTTP | Description |
|---|---|---|
validation_error | 400 | One or more fields failed validation. See errors for details. |
amount_below_minimum | 400 | Payment amount is below the ₦100 minimum. |
insufficient_wallet_balance | 400 | Not enough wallet balance. |
authentication_required | 401 | No Authorization header was sent. |
invalid_api_key | 401 | API key not recognised or inactive. |
live_mode_not_enabled | 403 | KYC verification not complete. |
transfer_not_allowed | 403 | Transfer attempted with a test key. |
payment_not_found | 404 | No payment matches the given reference. |
refund_not_found | 404 | No refund matches the given reference. |
idempotency_conflict | 409 | Same idempotency_key used for a different request. |
unprocessable_entity | 422 | Request valid but cannot be fulfilled (e.g. already-refunded payment). |
rate_limit_exceeded | 429 | Too many requests. Wait for Retry-After seconds. |
internal_error | 500 | BursaPay server error. Include request_id when contacting support. |
provider_error | 502 | Payment provider error. Retry after a delay. |
gateway_timeout | 504 | Provider timed out. Verify before retrying. |
Rate Limits
| Scope | Limit | Window |
|---|---|---|
Test API keys (sk_test_, pk_test_) | 100 req | per minute |
Live API keys (sk_live_, pk_live_) | 1,000 req | per minute |
| Per IP address (all traffic) | 300 req | per minute |
When a limit is exceeded you receive 429 with a Retry-After: N header telling you how many seconds to wait.
Handling Errors in Code
const res = await fetch('https://api.bursapay.com/api/v1/payments/initialize/', {
method: 'POST',
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 5000, email: 'john@example.com' }),
});
const body = await res.json();
if (!body.success) {
switch (body.code) {
case 'validation_error':
console.error('Fix these fields:', body.errors);
break;
case 'rate_limit_exceeded':
console.warn('Rate limited — retry in', res.headers.get('Retry-After'), 's');
break;
case 'live_mode_not_enabled':
console.error('Complete KYC verification in the portal to use live keys.');
break;
case 'gateway_timeout':
case 'provider_error':
case 'internal_error':
console.error('Server-side error, retry:', body.request_id);
break;
default:
console.error(`[${body.code}] ${body.message}`);
}
} else {
window.location.href = body.data.authorization_url;
}
Official SDKs
Stop writing raw HTTP calls. The official BursaPay SDKs wrap every endpoint with typed methods, structured error objects, and built-in webhook verification — in Python, JavaScript/TypeScript, PHP, and Go.
Resources covered by all official SDKs
payments • customers • transfers • wallets • webhooks • payment_links • virtual_accounts • subscriptions • invoices • refunds • disputes • reconciliation
Python SDK
Python 3.8+ • Sync & Async
pip install bursapay-sdk
JavaScript / TypeScript
Node 18+ • ESM & CJS
npm install bursapay-sdk
PHP SDK
PHP 8.0+ • Guzzle • PSR-4
composer require bursapay/bursapay-php
Go SDK
Go 1.18+ • Contextual
go get github.com/bursapay/bursapay-go
Python Quick Start
from bursapay import BursaPay
bp = BursaPay("sk_test_xxxx") # or sk_live_xxxx
# Payments
payment = bp.payments.initialize(amount=5000, email="customer@example.com")
result = bp.payments.verify("BP-XXXX")
print(result["status"]) # "success" | "failed" | "pending"
# Customers
customer = bp.customers.create(email="ada@example.com", name="Ada Okafor")
page = bp.customers.list(q="ada", page_size=50)
# Transfers
bp.transfers.initiate(amount=10000, bank_code="044",
account_number="0123456789", account_name="John Doe")
# Wallets
balance = bp.wallets.balance()
ledger = bp.wallets.ledger(entry_type="credit")
import asyncio
from bursapay import BursaPay
async def main():
async with BursaPay("sk_test_xxxx").async_client() as bp:
payment = await bp.payments.initialize(amount=5000, email="a@b.com")
result = await bp.payments.verify(payment["reference"])
balance = await bp.wallets.balance()
asyncio.run(main())
from bursapay import BursaPay
is_valid = BursaPay.verify_webhook_signature(
request.body,
request.headers.get("X-BursaPay-Signature", ""),
"your_webhook_secret"
)
if not is_valid:
raise PermissionError("Invalid signature")
import json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from bursapay import BursaPay
@csrf_exempt
def bursapay_webhook(request):
is_valid = BursaPay.verify_webhook_signature(
request.body, request.headers.get("X-BursaPay-Signature", ""), "secret"
)
if not is_valid:
return HttpResponse(status=401)
event = json.loads(request.body)
return HttpResponse(status=200)
from fastapi import FastAPI, Request, HTTPException
from bursapay import BursaPay
app = FastAPI()
@app.post("/hooks/bursapay/")
async def bursapay_webhook(request: Request):
body = await request.body()
sig = request.headers.get("X-BursaPay-Signature", "")
if not BursaPay.verify_webhook_signature(body, sig, "secret"):
raise HTTPException(status_code=401)
return {"status": "ok"}
JavaScript / TypeScript Quick Start
import { BursaPay } from 'bursapay-sdk';
const bp = new BursaPay('sk_test_xxxx');
// Payments
const payment = await bp.payments.initialize({
amount: 5000, email: 'customer@example.com', currency: 'NGN'
});
const result = await bp.payments.verify('BP-XXXX');
// Customers & Transfers
const customer = await bp.customers.create({ email: 'ada@example.com', name: 'Ada' });
await bp.transfers.initiate({ amount: 10000, bank_code: '044', account_number: '0123456789' });
import { BursaPay, Payment, Currency } from 'bursapay-sdk';
const bp = new BursaPay(process.env.BURSAPAY_SECRET_KEY!);
const payment: Payment = await bp.payments.initialize({
amount: 5000,
email: 'customer@example.com',
currency: 'NGN' satisfies Currency
});
import { BursaPayInline } from 'bursapay-sdk';
BursaPayInline.checkout({
publicKey: 'pk_live_xxxx',
reference: 'BP-XXXX',
email: 'customer@example.com',
onSuccess: (ref) => { window.location.href = `/confirm?ref=${ref}`; }
});
import express from 'express';
import { BursaPay } from 'bursapay-sdk';
const app = express();
app.post('/hooks/bursapay', express.raw({ type: '*/*' }), async (req, res) => {
const ok = await BursaPay.verifyWebhookSignature(req.body, req.headers['x-bursapay-signature'], process.env.WEBHOOK_SECRET);
if (!ok) return res.sendStatus(401);
res.sendStatus(200);
});
import { NextRequest, NextResponse } from 'next/server';
import { BursaPay } from 'bursapay-sdk';
export async function POST(request: NextRequest) {
const raw = Buffer.from(await request.arrayBuffer());
const sig = request.headers.get('x-bursapay-signature') ?? '';
const ok = await BursaPay.verifyWebhookSignature(raw, sig, process.env.WEBHOOK_SECRET!);
if (!ok) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
return NextResponse.json({ received: true });
}
PHP Quick Start
<?php
require 'vendor/autoload.php';
use BursaPay\BursaPay;
$bursapay = new BursaPay('sk_test_xxxx');
// Initialize Payment
$response = $bursapay->payments->initialize([
'amount' => 5000.00,
'email' => 'customer@example.com',
'currency' => 'NGN',
'callback_url' => 'https://yoursite.com/callback'
]);
$checkoutUrl = $response['data']['authorization_url'];
$reference = $response['data']['reference'];
// Verify Payment
$verification = $bursapay->payments->verify($reference);
if ($verification['data']['status'] === 'success') {
echo "Payment verified!";
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use BursaPay\Resources\Webhooks;
use BursaPay\Exceptions\SignatureVerificationException;
class WebhookController extends Controller
{
public function handle(Request $request)
{
$payload = $request->getContent();
$sig = $request->header('X-BursaPay-Signature', '');
$ts = $request->header('X-BursaPay-Timestamp', '');
$secret = config('services.bursapay.webhook_secret');
try {
$event = Webhooks::constructEvent($payload, $sig, $ts, $secret);
if ($event['event'] === 'payment.success') {
// Fulfill order...
}
return response()->json(['status' => 'ok']);
} catch (SignatureVerificationException $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}
<?php
use BursaPay\Resources\Webhooks;
$payload = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_BURSAPAY_SIGNATURE'] ?? '';
$ts = $_SERVER['HTTP_X_BURSAPAY_TIMESTAMP'] ?? '';
$secret = getenv('BURSAPAY_WEBHOOK_SECRET');
$isValid = Webhooks::verifySignature($payload, $sig, $ts, $secret);
if (!$isValid) {
http_response_code(400);
exit('Invalid signature');
}
Go Quick Start
package main
import (
"context"
"fmt"
"log"
"github.com/bursapay/bursapay-go"
)
func main() {
client := bursapay.NewClient("sk_test_xxxx")
ctx := context.Background()
// Initialize Payment
payment, err := client.Payments.Initialize(ctx, &bursapay.PaymentInitRequest{
Amount: 5000.00,
Email: "customer@example.com",
Currency: "NGN",
})
if err != nil {
log.Fatalf("Init failed: %v", err)
}
fmt.Printf("Checkout URL: %s\n", payment.AuthorizationURL)
// Verify Payment
verification, err := client.Payments.Verify(ctx, payment.Reference)
if err == nil && verification.Status == "success" {
fmt.Println("Payment verified!")
}
}
package main
import (
"io"
"net/http"
"os"
"github.com/bursapay/bursapay-go"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
sig := r.Header.Get("X-BursaPay-Signature")
ts := r.Header.Get("X-BursaPay-Timestamp")
secret := os.Getenv("BURSAPAY_WEBHOOK_SECRET")
event, err := bursapay.ConstructEvent(payload, sig, ts, secret)
if err != nil {
http.Error(w, "Invalid signature: "+err.Error(), http.StatusBadRequest)
return
}
if event.Event == "payment.success" {
// Fulfill order...
}
w.WriteHeader(http.StatusOK)
}
package main
import (
"log"
"github.com/bursapay/bursapay-go"
)
func verifyPayload(body []byte, sig, ts, secret string) {
err := bursapay.VerifySignature(body, sig, ts, secret, 300)
if err != nil {
log.Fatalf("Webhook signature invalid: %v", err)
}
log.Println("Signature verified successfully!")
}
Error Handling
All official SDKs map HTTP status codes to typed exceptions and error structures.
| Exception / Struct | HTTP Code | When |
|---|---|---|
ValidationError / InvalidRequest | 400 / 422 | Bad parameters — inspect error fields |
AuthenticationError | 401 / 403 | Invalid API key or restricted IP |
NotFoundError | 404 | Requested resource reference does not exist |
RateLimitError | 429 | API rate limit exceeded |
ServerError / APIError | 5xx | Infrastructure or upstream provider error |
from bursapay.exceptions import (
ValidationError, AuthenticationError, RateLimitError, ServerError
)
try:
payment = bp.payments.initialize(amount=5000, email="x@y.com")
except ValidationError as e:
print(e.error_code, e.field_errors)
except AuthenticationError:
print("Invalid API key")
except RateLimitError:
import time; time.sleep(2)
except ServerError as e:
print(f"Server error: {e.status_code}")
import { BursaPay, ValidationError, AuthenticationError, RateLimitError } from 'bursapay-sdk';
try {
const payment = await bp.payments.initialize({ amount: 5000, email: 'x@y.com' });
} catch (err) {
if (err instanceof ValidationError) {
console.log(err.errorCode, err.fieldErrors);
} else if (err instanceof AuthenticationError) {
console.log('Invalid key');
} else if (err instanceof RateLimitError) {
await new Promise(r => setTimeout(r, 2000));
}
}
<?php
use BursaPay\Exceptions\AuthenticationException;
use BursaPay\Exceptions\InvalidRequestException;
use BursaPay\Exceptions\RateLimitException;
use BursaPay\Exceptions\BursaPayException;
try {
$payment = $bursapay->payments->initialize(['amount' => 5000, 'email' => 'x@y.com']);
} catch (AuthenticationException $e) {
echo "Auth error: " . $e->getMessage();
} catch (InvalidRequestException $e) {
echo "Validation error: " . $e->getMessage();
} catch (RateLimitException $e) {
echo "Rate limited. Retry later.";
} catch (BursaPayException $e) {
echo "API Error: " . $e->getMessage();
}
package main
import (
"context"
"errors"
"fmt"
"github.com/bursapay/bursapay-go"
)
func main() {
client := bursapay.NewClient("sk_test_xxxx")
_, err := client.Payments.Initialize(context.Background(), &bursapay.PaymentInitRequest{Amount: 5000})
var authErr *bursapay.AuthenticationError
var invalidErr *bursapay.InvalidRequestError
var rateErr *bursapay.RateLimitError
if errors.As(err, &authErr) {
fmt.Println("Auth failed:", authErr.Error())
} else if errors.As(err, &invalidErr) {
fmt.Println("Invalid input:", invalidErr.Error())
} else if errors.As(err, &rateErr) {
fmt.Println("Rate limit exceeded:", rateErr.Error())
}
}
Best Practices
✓ Use environment variables
Never hard-code API keys. Store in .env and read via process.env, os.getenv(), or os.Getenv().
✓ Always verify server-side
Never trust browser callbacks alone. Always call payments.verify() from your server.
✓ Use idempotency keys
Pass unique idempotency_key on payments and refunds to safely retry network failures.
⚠ Verify webhook signatures
Always validate X-BursaPay-Signature using the SDK's verify method.
⚡ BursaPay CLI (bursapay-cli)
Test webhooks locally without public URLs, tunnel real-time events to localhost, replay log files, and trigger mock events directly from your terminal.
Instant Execution — No Installation Needed
Run directly via npx bursapay-cli <command> on Mac, Windows, or Linux. Node.js 16+ required.
🎧 Webhook Listening & Tunneling
Stream live events to localhost with automatic HMAC signatures
npx bursapay-cli listen --forward-to http://localhost:5000/webhooks/bursapay
Output Preview:
✓ 200 OK (18ms) payment.success
🔍 Webhook Signature Inspector
Inspect & test HMAC SHA-256 signatures and timestamp freshness locally
npx bursapay-cli verify --signature v1=8da6... --body '{"event":"payment.success"}'
Output Preview:
🚀 Event Triggering
Exercise full server signing pipeline & local handler
npx bursapay-cli trigger payment.success --forward-to http://localhost:5000/webhooks/bursapay
Output Preview:
✓ 200 OK Delivered to local server in 14ms!
💳 Sandbox Mock Data Generator
Instant test cards, virtual accounts, USSD codes & customers
npx bursapay-cli mock card
Output Preview:
💳 Verve (Success): 5061 0000 0000 0000 (Exp: 06/27, CVV: 789)
CLI Commands Reference
| Command | Description | Example Usage |
|---|---|---|
login |
Saves your secret API key locally to ~/.bursapay/config.json |
npx bursapay-cli login |
logout |
Clears saved API key from local config | npx bursapay-cli logout |
listen |
Connects to SSE stream and forwards live events to localhost | npx bursapay-cli listen --filter payment.success,refund.completed |
trigger |
Triggers a mock event through server pipeline & local endpoint | npx bursapay-cli trigger subscription.charged --forward-to http://localhost:8000/webhooks/ |
replay |
Staggered re-forwarding of saved JSONL log files to local server | npx bursapay-cli replay --log-file ./events.jsonl --delay 200 |
verify |
Inspects & tests webhook HMAC SHA-256 signatures locally | npx bursapay-cli verify --signature v1=a7e520... --body '{"event":"payment.success"}' |
mock |
Generates sandbox test cards, virtual accounts, USSD codes & customers | npx bursapay-cli mock card |
ip |
Displays public IP and checks against API key allowlist rules | npx bursapay-cli ip |
status |
Checks server connectivity, API key validity & account stream limits | npx bursapay-cli status --api-key sk_test_xxxx |
CLI Flags & Options
| Flag | Description | Default |
|---|---|---|
--forward-to <url> |
Local HTTP endpoint to forward incoming webhooks to | http://localhost:8000/webhooks/ |
--api-key <key> |
Your secret API key (sk_test_... or sk_live_...) |
$BURSAPAY_SECRET_KEY |
--env <test|live> |
Environment mode validation | test |
--filter <events> |
Comma-separated list of event types to forward | All events |
--since <time> |
Replay events since relative time (e.g. 15m, 1h) or ISO timestamp |
Latest |
--delay <ms> |
Delay between replayed events during bursapay replay |
100ms |
--log-file <path> |
Append incoming events to disk in JSON Lines format (.jsonl) |
None |
🚀 Demo Projects
See these APIs in action with real, runnable code across Python, Node.js, Next.js, Go, and PHP. Each of the 10 demo projects ships in two variants — one using the official BursaPay SDK and one using raw HTTP — so you can pick the pattern that fits your stack.
Quick Reference
https://api.bursapay.com/api/v1/Bearer Tokenapplication/json100 req/min (test)1,000 req/min2026-07-24Response Headers
test or live