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., ["PALMPAY", "OPAY"]) | Required |
merchantID |
string | Your merchant ID | 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: '+2348012345678',
bankCode: ['PALMPAY'],
merchantID:
merchantID
};
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);
});
Supported Banks
| Bank Code | Bank Name | Status |
|---|---|---|
PALMPAY |
PalmPay | Active |
OPAY |
OPay | Active |
20897 |
Other Bank | 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 | Destination bank code | Required |
account_number |
string | 10-digit NUBAN account number | Required |
account_name |
string | Name of the account holder | Required |
description |
string | Narration for the transaction | Optional |
reference |
string | Unique transaction reference | 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: "058",
account_number: "0123456789",
account_name: "Sadeeq Doe",
description: "Salary Payment",
reference: "PAY_123456789"
})
});
const data = await payout.json();
console.log(data);
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, max: 1440) | 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": 125.00,
"net_amount": 4875.00,
"currency": "NGN",
"payment_account": {
"account_number": "8012345678",
"account_name": "KatPay John",
"bank_name": "PalmPay"
},
"checkout_url": "https://checkout.katpay.co/pay/transfer/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customer": {
"name": "John Doe",
"email": "john@example.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 = 'sk_live_your_secret_key';
const response = await axios.post(
'https://api.katpay.co/v1/transfer-payments',
{
amount: 5000,
customer_name: 'John Doe',
customer_email: 'john@example.com',
customer_phone: '+2348012345678',
callback_url: 'https://yoursite.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: 8012345678
// Bank: PalmPay
// 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 queries PalmPay every ~12s and falls back to DB 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": 125.00,
"net_amount": 4875.00,
"currency": "NGN",
"customer_name": "John Doe",
"customer_email": "john@example.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": "JOHN DOE",
"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 = 'sk_live_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 |
transaction.completed |
Triggered when a transaction is successfully completed |
payout.processed |
Triggered when a payout has been processed |
transfer_payment.completed |
Triggered when a pay-with-transfer payment is confirmed (sent to your callback_url) |
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_1678901234_a1b2c3d4",
"timestamp": 1678901234,
"data": {
"virtual_account": {
"account_number": "1234567890",
"account_name": "Sadeeq Doe",
"bank_name": "PalmPay",
"provider_reference": "ref_123456"
},
"customer": {
"email": "customer@example.com",
"phone": "08012345678",
"name": "Sadeeq Doe"
},
"transaction": {
"order_no": "ORD-123456789",
"order_status": "SUCCESS",
"order_amount": 5000.00,
"order_amount_cents": 500000,
"currency": "NGN",
"created_time": 1678901200,
"update_time": 1678901234,
"reference": "ref_123456",
"session_id": "sess_123456"
},
"payer": {
"account_number": "0123456789",
"account_name": "Jane Doe",
"bank_name": "GTBank"
},
"merchant": {
"merchant_id": "MERCH-123456",
"business_name": "My Business"
}
}
}
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.