Katpay provides a robust API for processing payments in Nigeria. Our API allows you to create virtual accounts, process transactions, and manage payouts securely.
Katpay API Documentation
Build secure payment solutions with Katpay's comprehensive API. Process payments, manage virtual accounts, and handle payouts with ease.
Overview
Authentication
All API requests require authentication using your API keys. Keep your keys secure and never expose them in client-side code.
Required Headers
| Header | Description | Required |
|---|---|---|
Authorization |
Bearer token with your API secret | Required |
api-key |
Your public API key | Required |
Content-Type |
Must be application/json |
Required |
const headers = {
'Authorization': `Bearer ${apiSecret}`,
'Content-Type': 'application/json',
'api-key': apiKey
};
Virtual Accounts
Create static virtual accounts for your customers. These accounts are tied to your merchant account and allow you to receive payments.
Request Body
| Parameter | Type | Description | Required |
|---|---|---|---|
email |
string | Customer's email address | Required |
name |
string | Customer's full name | Required |
phoneNumber |
string | Customer's phone number | Required |
bankCode |
array | Array of bank codes (e.g., ["WEMA", "TITAN"]). KatPay issues the customer's account numbers and returns every one of them in accounts. |
Required |
merchantID |
string | Your merchant ID | Required |
bvn |
string | Customer's 11-digit Bank Verification Number | Required |
bvn — it must belong to the customer the account is being created for.
The account is registered against this identity.
Send it as a string (e.g. "01234567890") so leading zeros are kept. A request without a valid 11-digit BVN is rejected with 422:
{
"success": false,
"message": "The customer's BVN is required.",
"errors": { "bvn": ["The customer's BVN is required."] }
}
const axios = require('axios');
const apiKey = 'your-public-key';
const apiSecret = 'your-api-secret';
const merchantID = 'your-merchant-id';
const headers = {
'Authorization': `Bearer ${apiSecret}`,
'Content-Type': 'application/json',
'api-key': apiKey
};
const data = {
email: 'customer@example.com',
name: 'Sadeeq Doe',
phoneNumber: '+2349055439338',
bankCode: ['WEMA', 'TITAN'],
merchantID: merchantID,
bvn: '12345678901' // the customer's BVN
};
axios.post('https://api.katpay.co/v1/virtual-accounts', data, { headers })
.then(response => {
console.log('Virtual account created:', response.data);
})
.catch(error => {
console.error('Error:', error.response.data);
});
Response
The account name carries the customer's name (for customer Sadeeq Doe: KATPAY/DOE SADEEQ), so payers can see who they are paying.
A customer can receive more than one account number (one per partner bank). The top-level fields describe the
primary account; accounts lists every number the customer can pay into — show them all.
Calling the endpoint again for the same customer returns the same numbers with "already_exists": true (HTTP 200).
{
"success": true,
"message": "Virtual account created successfully",
"data": {
"account_number": "9930012345",
"account_name": "KATPAY/DOE SADEEQ",
"bank_name": "Wema Bank",
"provider_reference": "PSDVA_123456",
"status": "active",
"total_received": "0.00",
"transaction_count": 0,
"last_transaction_at": null,
"created_at": "2026-10-14T10:00:00.000000Z",
"already_exists": false,
"accounts": [
{
"account_number": "9930012345",
"account_name": "KATPAY/DOE SADEEQ",
"bank_name": "Wema Bank",
"provider_reference": "PSDVA_123456",
"status": "active"
},
{
"account_number": "9881234567",
"account_name": "KATPAY/DOE SADEEQ",
"bank_name": "Paystack-Titan",
"provider_reference": "PSDVA_123457",
"status": "active"
}
]
}
}
Supported Banks
| Bank Code | Bank Name | Status |
|---|---|---|
WEMA |
Wema Bank | Active |
TITAN |
Paystack-Titan | Active |
Payouts
Programmatically send money to bank accounts in Nigeria. This feature must be enabled in your Merchant Settings.
Create Payout
Request Body
| Parameter | Type | Description | Required |
|---|---|---|---|
amount |
number | Amount to transfer in Naira (min 100) | Required |
bank_code |
string | 6-digit bank code from the Bank Codes list (e.g. 000013 for GTBank) |
Required |
account_number |
string | 10-digit NUBAN account number | Required |
account_name |
string | Name of the account holder. The bank's record is checked before sending; a payout to an account under a different name is refused and your balance refunded. | Required |
description |
string | Narration for the transaction | Optional |
reference |
string | Your unique reference (letters, numbers, - and _, max 100). Reusing one returns 409. Generated for you if omitted. |
Optional |
Example Request
const payout = await fetch('https://api.katpay.co/v1/payouts', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_SECRET_KEY',
'Content-Type': 'application/json',
'api-key': 'YOUR_PUBLIC_KEY'
},
body: JSON.stringify({
amount: 5000,
bank_code: "000013",
account_number: "0123456789",
account_name: "Sadeeq Doe",
description: "Salary Payment",
reference: "PAY_123456789"
})
});
const data = await payout.json();
console.log(data);
Responses
The amount plus the payout fee (fee, total_deducted) is reserved from your balance as soon as the request is accepted.
Most payouts answer 202 with status processing: the money is on its way and the bank's confirmation follows,
usually within minutes. Check the final status with GET /v1/payouts/{reference}. If a payout fails, the amount and the fee are returned
to your balance automatically.
{
"success": true,
"status": "processing",
"message": "Payout is processing. The final status will be confirmed shortly.",
"data": {
"reference": "PAY_123456789",
"status": "processing"
}
}
{
"success": false,
"message": "Payout could not be processed. Your balance has been refunded.",
"data": {
"reference": "PAY_123456789",
"status": "failed"
}
}
| HTTP | Meaning |
|---|---|
200 | Payout completed immediately (status: completed) |
202 | Accepted and processing — check GET /v1/payouts/{reference} for the result |
400 | Payout failed and was refunded, or not enough withdrawable balance (the message gives the amount available to withdraw now) |
403 | Payouts are not enabled, or are locked, for your account |
409 | Duplicate reference |
422 | Validation error (see errors) |
429 | Too many requests — payouts are limited to 5 per minute |
503 | Payouts are temporarily paused — try again later |
Get Payout Status
processing, completed or failed (failed payouts are already refunded).
Payout results are not sent by webhook — poll this endpoint (for example every minute) until the status is final.
{
"success": true,
"data": {
"reference": "PAY_123456789",
"status": "completed",
"amount": 5000,
"fee": 20,
"total_deducted": 5020,
"currency": "NGN",
"recipient": {
"bank_code": "000013",
"bank_name": "N/A",
"account_number": "******6789",
"account_name": "SADEEQ DOE"
},
"description": "Salary Payment",
"created_at": "2026-10-14T10:00:00+01:00",
"processed_at": "2026-10-14T10:00:01+01:00",
"completed_at": "2026-10-14T10:02:30+01:00"
}
}
Check Balance
available is the amount you can withdraw right now — check it before sending a payout.
{
"success": true,
"data": {
"merchant_id": "MERCH-123456",
"business_name": "My Business",
"balance": 150000,
"frozen": 0,
"available": 42500,
"currency": "NGN",
"payout_enabled": true,
"payout_locked": false,
"timestamp": "2026-10-14T10:00:00+01:00"
}
}
Pay with Transfer
Accept one-time payments from customers via direct bank transfer. Each payment request generates a unique payment order — no shared accounts, no ambiguity. Your system initiates a payment, and gets notified automatically when the customer completes the transfer.
How It Works
- Your server calls
POST /v1/transfer-paymentswith amount, customer info, and callback URL - KatPay creates a unique payment order and returns bank account details and a
checkout_url - Redirect customer to the
checkout_url(KatPay hosted checkout — timer, copy button, auto-detect) - Alternatively, build your own UI using the
payment_accountdetails from the response - Customer makes a bank transfer to the provided account
- KatPay confirms the payment and sends a signed callback to your
callback_urland your dashboard webhook URL - Your server verifies the signature and completes the order
checkout_url returned in the response. KatPay handles the entire payment UI, countdown timer, and status updates — no frontend code needed.
Headers
| Header | Value | Description |
|---|---|---|
Authorization |
Bearer {secret_key} |
Your live or test secret key |
Content-Type |
application/json |
Required for all requests |
Request Body
| Parameter | Type | Description | Required |
|---|---|---|---|
amount |
number | Amount in Naira (min: 100, max: 10,000,000) | Required |
customer_name |
string | Customer's full name | Required |
customer_email |
string | Customer's email address | Required |
callback_url |
string | URL to receive payment confirmation (must be HTTPS in production) | Required |
merchant_reference |
string | Your unique reference for this payment (e.g., order ID) | Required |
customer_phone |
string | Customer's phone number | Optional |
description |
string | Payment description (max 500 chars) | Optional |
success_url |
string | URL to redirect customer after successful payment | Optional |
metadata |
object | Custom key-value data returned in the callback | Optional |
expires_in |
integer | Minutes until payment expires (default: 30; between 15 and 480 — the account stays open for 15 minutes to 8 hours) | Optional |
currency |
string | Currency code (default: NGN) | Optional |
Success Response (201)
{
"success": true,
"message": "Transfer payment initiated successfully",
"data": {
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"merchant_reference": "ORDER_001",
"internal_reference": "TRF_ABCDEFGHIJ1234567890_1711000000",
"status": "pending",
"amount": 5000.00,
"fee_amount": 75.00,
"net_amount": 4925.00,
"currency": "NGN",
"payment_account": {
"account_number": "9887985018",
"account_name": "PAYSTACK CHECKOUT",
"bank_name": "Paystack-Titan"
},
"checkout_url": "https://checkout.katpay.co/pay/transfer/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customer": {
"name": "sadeeq dev",
"email": "kathost@gmail.com"
},
"expires_at": "2026-03-21T10:30:00+00:00",
"created_at": "2026-03-21T10:00:00+00:00"
}
}
const axios = require('axios');
const SECRET_KEY = 'live_your_secret_key';
const response = await axios.post(
'https://api.katpay.co/v1/transfer-payments',
{
amount: 5000,
customer_name: 'Sadeeq dev',
customer_email: 'Sadeeq@example.com',
customer_phone: '+2349055439338',
callback_url: 'https://yourdomainsite.com/payment/callback',
merchant_reference: 'ORDER_001',
description: 'Payment for Order #001',
metadata: {
order_id: '001',
product: 'Premium Plan'
},
expires_in: 30
},
{
headers: {
'Authorization': `Bearer ${SECRET_KEY}`,
'Content-Type': 'application/json'
}
}
);
console.log('Payment account:', response.data.data.payment_account);
// Show these details to your customer:
// Account Number: 9887985018
// Bank: Paystack-Titan
// Amount: NGN 5,000
Hosted Checkout (Recommended)
The easiest way to accept bank transfer payments — just redirect your customer to the checkout_url
returned in the initiation response. KatPay provides a fully hosted payment page with:
- Bank account details with one-click copy
- Real-time countdown timer with expiry warning
- Automatic payment detection — actively checks for the payment every ~12s and falls back to polling every 4s
- “I have made this transfer” button for instant verification on demand
- Auto-redirect to your
success_urlwhen payment completes - Fully responsive — mobile-optimized UI your customers can trust
// After calling POST /v1/transfer-payments
const result = await response.json();
// Redirect customer to KatPay's hosted checkout (recommended)
window.location.href = result.data.checkout_url;
// Or build your own UI using result.data.payment_account
Checkout URL format: https://checkout.katpay.co/pay/transfer/{uuid}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
uuid |
string | The UUID returned when initiating the transfer payment |
Payment Status Values
| Status | Description |
|---|---|
pending |
Waiting for customer to make the transfer |
processing |
Transfer detected, payment is being confirmed |
completed |
Payment confirmed — merchant wallet credited |
expired |
Payment window timed out with no transfer |
cancelled |
Cancelled by merchant via API |
failed |
Payment processing encountered an error |
GET /transaction/verify/:reference.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
merchant_reference |
string | Your unique reference passed when initiating the transfer payment |
Response includes
| Field | Description |
|---|---|
amount |
The amount you requested (expected amount) |
amount_received |
The actual amount the customer transferred (may differ from requested) |
status |
Current payment status (pending, completed, etc.) |
pending.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status (pending, completed, expired, cancelled) |
merchant_reference |
string | Filter by your merchant reference |
per_page |
integer | Results per page (default: 20, max: 100) |
Callback Notification
When a customer completes the transfer, KatPay sends a POST request to your callback_url with the payment details. The callback is signed so you can verify it came from KatPay.
callback_url and your dashboard-configured Webhook URL (if set). This ensures your server is always notified even if the callback_url is inaccessible.
Callback Headers
| Header | Description |
|---|---|
X-KatPay-Signature |
HMAC-SHA256 signature for verification |
X-KatPay-Timestamp |
Unix timestamp when callback was sent |
X-KatPay-Event |
Always transfer_payment.completed |
X-KatPay-Delivery-Id |
Unique ID for this delivery (for idempotency) |
Callback Body
{
"event": "transfer_payment.completed",
"data": {
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"merchant_reference": "ORDER_001",
"internal_reference": "TRF_ABCDEFGHIJ1234567890_1711000000",
"status": "completed",
"amount": 5000.00,
"amount_received": 5000.00,
"fee_amount": 75.00,
"net_amount": 4925.00,
"currency": "NGN",
"customer_name": "sadeeq dev",
"customer_email": "kathost@gmail.com",
"description": "Payment for Order #001",
"metadata": {
"order_id": "001",
"product": "Premium Plan"
},
"paid_at": "2026-03-21T10:05:00+00:00",
"completed_at": "2026-03-21T10:05:01+00:00",
"payer_account_name": "sadeeq dev",
"payer_bank_name": "GTBank"
}
}
Verifying the Callback Signature
Always verify the X-KatPay-Signature header to ensure the callback is authentic. The signature is computed as: HMAC-SHA256(timestamp + "." + payload, your_secret_key)
<?php
// Your callback endpoint (e.g., https://yoursite.com/payment/callback)
$SECRET_KEY = 'your_secret_key'; // Your KatPay Secret Key
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_KATPAY_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_KATPAY_TIMESTAMP'] ?? '';
// 1. Verify the signature
$expectedSignature = hash_hmac('sha256', $timestamp . '.' . $payload, $SECRET_KEY);
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// 2. Prevent replay attacks (reject if older than 5 minutes)
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(401);
echo json_encode(['error' => 'Timestamp too old']);
exit;
}
// 3. Parse the payload
$data = json_decode($payload, true);
$event = $data['event'];
$payment = $data['data'];
if ($event === 'transfer_payment.completed') {
$merchantReference = $payment['merchant_reference'];
$amount = $payment['amount'];
$status = $payment['status'];
$metadata = $payment['metadata'];
// Update your order in your database
// e.g., Order::where('reference', $merchantReference)->update(['status' => 'paid']);
// Return 200 to acknowledge receipt
http_response_code(200);
echo json_encode(['received' => true]);
} else {
http_response_code(200);
echo json_encode(['received' => true, 'note' => 'Unhandled event']);
}
- Always verify the signature before processing the callback
- Return HTTP
200to acknowledge receipt. KatPay retries up to 3 times on non-2xx responses - Use
merchant_referenceto match callbacks to your orders - Check the
statusfield — only processcompletedpayments - Use
X-KatPay-Delivery-Idfor idempotency to avoid processing duplicates - The
metadataobject contains whatever you passed during initiation
Supported Bank Codes
When making a payout, you need to pass the correct bank_code for the destination bank. Below is the full list of supported banks and their codes, fetched live from our payment processor.
Response Fields
| Field | Type | Description |
|---|---|---|
bankCode |
string | Unique code for the bank (use this in payouts) |
bankName |
string | Full name of the bank |
Bank Code Directory
Search the table below to find the correct bank code for your payout destination.
Loading bank list...
Webhooks
Webhooks allow you to receive real-time notifications when events occur in your Katpay account. Set up webhook endpoints to automatically process payments, track transactions, and handle account updates.
Webhook Events
| Event Type | Description |
|---|---|
virtual_account.payment_received |
Triggered when a payment is received into a virtual account |
transfer_payment.completed |
Triggered when a pay-with-transfer payment is confirmed (sent to your callback_url) |
Payout results are not sent by webhook — check them with GET /v1/payouts/{reference}.
Required Headers
| Header | Description |
|---|---|
X-Katpay-Signature |
HMAC SHA256 signature for webhook verification |
X-Katpay-Timestamp |
Unix timestamp when the webhook was sent |
Webhook Handler Implementation
Implement webhook signature verification to ensure webhooks are authentic and from Katpay.
<?php
/**
* KatPay Webhook Handler
* Replace YOUR_SECRET_KEY with your Live Secret Key from KatPay
*/
// Your Live Secret Key from KatPay dashboard
$SECRET_KEY = 'your-webhook-secret-key';
// Get webhook data
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_KATPAY_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_KATPAY_TIMESTAMP'] ?? '';
header('Content-Type: application/json');
try {
// Validate secret key is set
if (empty($SECRET_KEY) || $SECRET_KEY === 'your-webhook-secret-key') {
http_response_code(500);
echo json_encode(['error' => 'Webhook secret not configured']);
exit;
}
// Validate required headers
if (empty($signature) || empty($timestamp)) {
http_response_code(400);
echo json_encode(['error' => 'Missing required headers']);
exit;
}
// Verify signature
$signedPayload = $timestamp . '.' . $payload;
$expectedSignature = hash_hmac('sha256', $signedPayload, $SECRET_KEY);
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process webhook
$data = json_decode($payload, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON payload']);
exit;
}
$eventType = $data['event_type'] ?? '';
// Handle payment received event
if ($eventType === 'virtual_account.payment_received') {
$transaction = $data['data']['transaction'] ?? [];
$customer = $data['data']['customer'] ?? [];
$amount = $transaction['order_amount'] ?? 0;
$orderNo = $transaction['order_no'] ?? '';
$transactionId = $transaction['id'] ?? '';
$customerEmail = $customer['email'] ?? '';
$customerPhone = $customer['phone'] ?? '';
// Your business logic here
// Example: Update database, send confirmation email, etc.
// Respond with success
http_response_code(200);
echo json_encode([
'status' => 'success',
'message' => 'Payment processed'
]);
} else {
// Handle other event types
http_response_code(200);
echo json_encode([
'status' => 'success',
'message' => 'Event received'
]);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Internal server error']);
}
?>
Example Payload
{
"event_type": "virtual_account.payment_received",
"event_id": "evt_1760436000_a1b2c3d4",
"timestamp": 1760436001,
"data": {
"virtual_account": {
"account_number": "9930012345",
"account_name": "KATPAY/DOE SADEEQ",
"bank_name": "Wema Bank",
"provider_reference": "PSDVA_123456"
},
"customer": {
"email": "customer@example.com",
"phone": "08012345678",
"name": "Sadeeq Doe"
},
"transaction": {
"id": "Q7TX2L9MZK4W8N1B5V3C6R0Y2P9S4D7F1G8H5J2K6L3M0N7B4V1",
"order_no": "100004241014100000012345",
"order_status": 1,
"order_amount": 5000,
"order_amount_cents": 500000,
"currency": "NGN",
"created_time": 1760436000000,
"update_time": 1760436001000,
"reference": "100004241014100000012345",
"session_id": ""
},
"payer": {
"account_number": "XXXXXX6789",
"account_name": "JANE DOE",
"bank_name": "Guaranty Trust Bank"
},
"merchant": {
"merchant_id": "MERCH-123456",
"business_name": "My Business"
},
"fees": {
"gross_amount": 5000,
"fee_amount": 75,
"net_amount": 4925,
"fee_percentage": 1.5,
"fee_breakdown": { "percentage": 75, "fixed": 0 }
},
"wallet": {
"balance_before": 10000,
"balance_after": 14925,
"balance_change": 4925
}
}
}
Security Best Practices
Protect Your API Keys
Never store API keys in your frontend code or public repositories.
Use HTTPS Only
Always use HTTPS for API requests to encrypt data in transit.
Monitor Webhooks
Implement webhook signature verification to ensure webhook authenticity.