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

  1. Initialize Payment: Call POST /payments/initialize/. Store the BP-... reference.
  2. Redirect: Redirect customer to authorization_url.
  3. Callback: Customer returns to your callback_url with reference.
  4. Verify: Call POST /payments/verify/ with the reference.
  5. 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.

ParameterTypeDescription
amountrequirednumberAmount in NGN. Minimum: 100.
emailrequiredstringCustomer's email address.
currencyoptionalstringISO 4217 code. Defaults to NGN.
callback_urloptionalstringURL to redirect after payment.
metadataoptionalobjectAny custom data to attach.
idempotency_keyoptionalstringPrevents duplicate charges.
splitsoptionalarrayMarketplace 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/

ParameterTypeDescription
pageintegerPage number
per_pageintegerRecords per page
statusstringPayment status
fromdateStart date
todateEnd date
qstringSearch 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/

ParameterTypeDescription
customer_reference *stringCustomer reference
amount *integerAmount
authorization_code *stringAuthorization code
metadataobjectAdditional metadata
Error CodeStatus CodeDescription
customer_not_found404Customer not found
charge_failed400Charge failed
gateway_timeout504Gateway timeout
provider_error502Provider 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/

ParameterTypeDescription
payments *arrayArray of payment objects
payments[].amountintegerAmount
payments[].customer_referencestringCustomer 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/

ParameterTypeDescription
email *stringCustomer email
namestringCustomer name
phonestringCustomer phone
metadataobjectAdditional 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

ColumnRequiredDescription
emailYesCustomer email — used as the unique identifier for upserts
nameNoFull name of the customer
phoneNoPhone number
metadataNoJSON 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

CodeHTTPDescription
validation_error400File field missing, empty, or CSV has no email header
file_too_large400Uploaded file exceeds the 5 MB limit
insufficient_scope403API 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.

ParameterTypeDescription
entry_typestringFilter by entry type
per_pageintegerRecords per page
cursorstringPagination cursor
fromdateStart date
todateEnd 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

Warning: Test mode webhooks should only be used in development. Ensure you use live mode webhooks for production.

Verify webhook events using HMAC signature.

EventDescription
payment.successFired when a payment succeeds
payment.failedFired when a payment fails
refund.completedFired when a refund completes
transfer.successFired when a transfer succeeds
transfer.failedFired when a transfer fails
subscription.chargedFired when a subscription charge succeeds
subscription.charge_failedFired when a subscription charge fails
subscription.cancelledFired when a subscription is cancelled
virtual_account.creditedFired when a virtual account receives funds
dispute.createdFired when a new dispute is created
dispute.wonFired when a dispute is resolved in your favour
dispute.lostFired when a dispute is resolved against you
withdrawal.completedFired when a withdrawal completes
customer.createdFired 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.

EndpointMethodDescription
/webhooks/POSTCreate a new webhook
/webhooks/GETList all webhooks
/webhooks/{id}/PATCHUpdate a webhook
/webhooks/{id}/DELETEDelete 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.

EventDescriptionStatus
payment.successA payment was completed successfullyAVAILABLE
payment.failedA payment attempt failedAVAILABLE
refund.initiatedA refund was initiatedAVAILABLE
refund.completedA refund completed successfullyAVAILABLE
refund.failedA refund failedAVAILABLE
transfer.successA payout transfer completed successfullyAVAILABLE
transfer.failedA payout transfer failedAVAILABLE
subscription.chargedA subscription charge succeededAVAILABLE
subscription.charge_failedA subscription charge failedAVAILABLE
subscription.cancelledA subscription was cancelledAVAILABLE
virtual_account.creditedA virtual account received fundsAVAILABLE
dispute.createdA new payment dispute was createdAVAILABLE
dispute.wonA dispute was resolved in your favourAVAILABLE
dispute.lostA dispute was resolved against youAVAILABLE
api_key.rotatedAn API key was rotatedAVAILABLE
webhook.testManual test eventAVAILABLE
subscription.createdA subscription was createdCOMING 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 ParameterTypeDescription
log_id requiredintegerThe 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/

ParameterTypeDescription
amount requirednumberAmount in NGN. Minimum ₦100.
bank_code requiredstringStandard bank code (e.g. "058" for GTBank). Call GET /transfers/banks/ for supported codes.
account_number requiredstring10-digit NUBAN account number.
account_name requiredstringAccount holder name as registered with the bank.
narration optionalstringDescription on recipient's bank statement. Max 255 chars.
metadata optionalobjectArbitrary 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
  }
}
HTTPCodeWhen
403transfer_not_allowedTest key used. Switch to sk_live_.
400insufficient_wallet_balanceWallet balance below requested amount.
400validation_errorInvalid field (non-numeric account, amount < 100).
502provider_errorNetwork 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.

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.

ParameterTypeDescription
period optionalstringtoday, 7d, 30d (default), 90d
currency optionalstringCurrency 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.

FieldTypeDescription
delivery_url optionalURL stringLink to delivery confirmation page or asset
delivery_confirmed_at optionalISO 8601 stringDatetime the delivery was confirmed, e.g. 2025-01-15T10:30:00Z
service_description optionalstringDescription of goods or services delivered (max 1000 chars)
proof_of_delivery optionalstringProof 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 fieldTypeRequiredDescription
scenariostringrequiredOne of basic, high_volume, disputes, subscriptions
ScenarioRecords created
basic10 payments, 5 customers
high_volume100 payments, 20 customers
disputes5 payments, 5 customers, 2 disputes
subscriptions3 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
  }
}
StatusCodeWhen
400invalid_scenarioMissing or unrecognised scenario value; response includes errors.valid_scenarios list
403live_mode_not_allowedRequest was made with a live key (sk_live_)
429seed_already_existsSame scenario was seeded within the last 24 hours; delete first or wait
500seed_failedDB 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

StatusMeaningWhen
200OKRequest succeeded.
201CreatedNew resource created.
400Bad RequestMissing/invalid fields or business rule violated.
401UnauthorizedNo API key, invalid key, or key deactivated.
403ForbiddenLive mode not enabled, key/mode mismatch, or IP blocked.
404Not FoundPayment, customer, or resource reference does not exist.
409ConflictIdempotency key already used for a different payload.
415Unsupported MediaContent-Type must be application/json.
422UnprocessableValid request but semantically impossible (e.g. refund exceeds original).
429Too Many RequestsRate limit hit. Check the Retry-After response header.
500Internal ErrorUnexpected server error. Log request_id and contact support.
502Provider ErrorPayment network returned unexpected error. Retry after a delay.
504Gateway TimeoutPayment network timed out. Safe to retry.

Common Error Codes

codeHTTPDescription
validation_error400One or more fields failed validation. See errors for details.
amount_below_minimum400Payment amount is below the ₦100 minimum.
insufficient_wallet_balance400Not enough wallet balance.
authentication_required401No Authorization header was sent.
invalid_api_key401API key not recognised or inactive.
live_mode_not_enabled403KYC verification not complete.
transfer_not_allowed403Transfer attempted with a test key.
payment_not_found404No payment matches the given reference.
refund_not_found404No refund matches the given reference.
idempotency_conflict409Same idempotency_key used for a different request.
unprocessable_entity422Request valid but cannot be fulfilled (e.g. already-refunded payment).
rate_limit_exceeded429Too many requests. Wait for Retry-After seconds.
internal_error500BursaPay server error. Include request_id when contacting support.
provider_error502Payment provider error. Retry after a delay.
gateway_timeout504Provider timed out. Verify before retrying.

Rate Limits

ScopeLimitWindow
Test API keys (sk_test_, pk_test_)100 reqper minute
Live API keys (sk_live_, pk_live_)1,000 reqper minute
Per IP address (all traffic)300 reqper 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 / StructHTTP CodeWhen
ValidationError / InvalidRequest400 / 422Bad parameters — inspect error fields
AuthenticationError401 / 403Invalid API key or restricted IP
NotFoundError404Requested resource reference does not exist
RateLimitError429API rate limit exceeded
ServerError / APIError5xxInfrastructure 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.

🐍 Python SDK on PyPI ⬡ JS SDK on npm 🐘 PHP SDK on GitHub 🐹 Go SDK on GitHub 📖 Full Developer Guide

⚡ 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:

READY Stream connected. Forwarding live webhooks -> http://localhost:5000/webhooks/
✓ 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:

✓ VERIFIED Signature matches! Webhook verification is working correctly.

🚀 Event Triggering

Exercise full server signing pipeline & local handler

npx bursapay-cli trigger payment.success --forward-to http://localhost:5000/webhooks/bursapay

Output Preview:

✓ Server trigger pipeline exercised (HMAC signed & logged)
✓ 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:

💳 Visa (Success): 4084 0000 0000 0000 (Exp: 12/28, CVV: 123)
💳 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.

Explore Integration Demos View all on GitHub →

Quick Reference

Base URLhttps://api.bursapay.com/api/v1/
AuthBearer Token
Content-Typeapplication/json
Rate Limit100 req/min (test)
Live Rate1,000 req/min
API Version2026-07-24

Response Headers

X-Request-IDInclude in support tickets
BursaPay-VersionAPI version used
X-BursaPay-Modetest or live