Build with Wittypay
Build secure payment solutions with Wittypay's comprehensive API. Accept payments, issue virtual accounts, and send payouts — all through a single REST API built for Nigeria.
Overview
Wittypay provides a robust API for collecting payments, issuing virtual accounts, and disbursing funds in Nigeria. All API calls are made over HTTPS to the base URL below.
| Property | Value |
|---|---|
| Base URL | https://wittypay.online/api/merchant/v1 |
| Protocol | HTTPS only |
| Data Format | JSON (request body & responses) |
| Authentication | Authorization: Bearer <secret_key> |
| Currency | Nigerian Naira (NGN). All amounts in Naira — e.g. 5000 = ₦5,000 |
| Encoding | UTF-8 |
Payments
Create checkout orders, accept card, transfer, and USSD payments.
Bank Transfer
Accept NIP bank transfers via hosted checkout or dedicated virtual accounts.
Virtual Accounts
Issue dedicated bank account numbers for permanent or one-time use.
Payouts
Disburse funds to any Nigerian bank account instantly.
Webhooks
Real-time signed notifications for every payment and payout event.
Customers
Store and manage customer profiles linked to transactions.
Authentication
All API requests must be authenticated using your secret API key. Generate keys from your merchant dashboard. Keep your secret key server-side — never expose it in frontend or mobile code.
sk_live_. Sandbox keys are prefixed sk_test_.Required Headers
| Header | Description | Required |
|---|---|---|
| Authorization | Bearer YOUR_SECRET_KEY | Required |
| Content-Type | application/json | Required |
# Pass your secret key in the Authorization header
curl -X GET "https://wittypay.online/api/merchant/v1/balance" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json"<?php
$secretKey = 'sk_live_YOUR_SECRET_KEY';
$headers = [
'Authorization: Bearer ' . $secretKey,
'Content-Type: application/json',
];
$ch = curl_init('https://wittypay.online/api/merchant/v1/balance');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
]);
$res = json_decode(curl_exec($ch), true);const SECRET_KEY = 'sk_live_YOUR_SECRET_KEY';
const headers = {
'Authorization': `Bearer ${SECRET_KEY}`,
'Content-Type': 'application/json',
};
const res = await fetch('https://wittypay.online/api/merchant/v1/balance', { headers });
const data = await res.json();import requests
SECRET_KEY = 'sk_live_YOUR_SECRET_KEY'
headers = {
'Authorization': f'Bearer {SECRET_KEY}',
'Content-Type': 'application/json',
}
r = requests.get('https://wittypay.online/api/merchant/v1/balance', headers=headers)
data = r.json()Error Handling
All errors return a JSON object with status: "error" and a human-readable message. Use the HTTP status code to categorise the error.
{
"status": "error",
"message": "Descriptive error message"
}
| HTTP Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created — payment order or payout initiated |
| 400 | Bad Request — missing or invalid field |
| 401 | Unauthorized — missing or invalid API key |
| 403 | Forbidden — key disabled or account not approved |
| 404 | Not Found — resource does not exist for your account |
| 405 | Method Not Allowed |
| 409 | Conflict — duplicate reference already exists |
| 422 | Unprocessable — account could not be verified |
| 500 | Internal Server Error |
| 502 | Bad Gateway — upstream payment processor error |
Payments (Checkout)
Create a payment order and redirect your customer to Wittypay's hosted checkout. Customers can pay via card, bank transfer, USSD, or mobile wallet. After payment, they are redirected to your callback_url and a signed webhook fires to your webhook_url.
| Parameter | Type | Description | Required |
|---|---|---|---|
| amount | number | Amount in NGN (minimum ₦100) | Required |
| reference | string | Your unique transaction reference | Required |
| callback_url | string | URL customer is redirected to after payment | Required |
| customer_name | string | Customer's full name | Optional |
| customer_email | string | Customer's email address | Optional |
| customer_phone | string | Customer's phone number | Optional |
| title | string | Payment title shown on checkout (default: Payment) | Optional |
| description | string | Additional payment description | Optional |
| notify_url | string | Override your account webhook URL for this order only | Optional |
| metadata | object | Custom key-value data returned in webhook | Optional |
curl -X POST "https://wittypay.online/api/merchant/v1/payments" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"reference": "ORD-20240115-001",
"callback_url": "https://yourdomain.com/payment/callback",
"customer_name": "Jane Doe",
"customer_email": "jane@example.com",
"customer_phone": "08012345678",
"title": "Order #001",
"metadata": { "order_id": 42 }
}'<?php
$ch = curl_init('https://wittypay.online/api/merchant/v1/payments');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 5000,
'reference' => 'ORD-20240115-001',
'callback_url' => 'https://yourdomain.com/payment/callback',
'customer_name' => 'Jane Doe',
'customer_email'=> 'jane@example.com',
]),
]);
$res = json_decode(curl_exec($ch), true);
if ($res['status'] === 'success') {
header('Location: ' . $res['data']['checkout_url']);
exit;
}const res = await fetch('https://wittypay.online/api/merchant/v1/payments', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 5000,
reference: 'ORD-20240115-001',
callback_url: 'https://yourdomain.com/payment/callback',
customer_name: 'Jane Doe',
customer_email:'jane@example.com',
}),
});
const data = await res.json();
// Redirect to: data.data.checkout_urlimport requests
r = requests.post(
'https://wittypay.online/api/merchant/v1/payments',
json={
'amount': 5000,
'reference': 'ORD-20240115-001',
'callback_url': 'https://yourdomain.com/payment/callback',
'customer_email':'jane@example.com',
},
headers={'Authorization': 'Bearer sk_live_YOUR_SECRET_KEY'},
)
data = r.json()
checkout_url = data['data']['checkout_url']Success Response (201)
{
"status": "success",
"data": {
"reference": "ORD-20240115-001",
"order_no": "PP20240115123456",
"checkout_url": "https://checkout.palmpay-inc.com/...",
"amount": 5000,
"currency": "NGN",
"status": "processing",
"expires_in": 3600
}
}
callback_url query parameters alone.| Query Parameter | Type | Description |
|---|---|---|
| ref | string | Your transaction reference Required* |
| order_no | string | Gateway order number (use either ref or order_no) |
curl "https://wittypay.online/api/merchant/v1/payments?ref=ORD-20240115-001" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
Response
{
"status": "success",
"data": {
"reference": "ORD-20240115-001",
"order_no": "PP20240115123456",
"status": "success",
"amount": 5000,
"fee": 75,
"net_amount": 4925,
"currency": "NGN",
"pay_method": "CARD",
"customer": {
"name": "Jane Doe",
"email": "jane@example.com"
},
"created_at": "2024-01-15 10:30:00",
"updated_at": "2024-01-15 10:31:22"
}
}
Bank Transfer
Accept payments via direct bank transfer. Each payment request generates a unique virtual account number — no shared accounts, no ambiguity. Your server initiates the payment, the customer transfers to the given account, and Wittypay notifies you automatically when the transfer clears.
How It Works
Initiate the payment
Your server calls POST /payments with the amount, customer info, and your callback_url.
Get the checkout URL
Wittypay returns a checkout_url pointing to a hosted payment page.
Redirect your customer
Send the customer to the checkout_url. On the page they select Bank Transfer and are shown a unique account number, USSD code, and QR code.
Customer makes the transfer
The customer transfers the exact amount from any Nigerian bank to the provided account number.
Wittypay confirms and notifies you
Wittypay detects the inbound transfer, credits your balance, and sends a signed payment.success webhook to your callback_url and your dashboard webhook URL.
Verify and fulfil
Your server verifies the webhook signature, calls GET /payments?ref=REFERENCE to confirm, then fulfils the order.
checkout_url returned in the response. Wittypay handles the entire payment UI, countdown timer, and status updates — no extra frontend code needed.| Parameter | Type | Description | Required |
|---|---|---|---|
| amount | number | Amount in NGN (min ₦100, max ₦10,000,000) | Required |
| reference | string | Your unique reference for this payment (e.g. order ID) | Required |
| callback_url | string | URL to redirect customer after payment and receive webhook | Required |
| customer_name | string | Customer's full name | Optional |
| customer_email | string | Customer's email address | Optional |
| customer_phone | string | Customer's phone number | Optional |
| title | string | Payment title shown on checkout | Optional |
| description | string | Payment description (max 500 chars) | Optional |
| metadata | object | Custom key-value data returned in webhook | Optional |
curl -X POST "https://wittypay.online/api/merchant/v1/payments" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"reference": "TRF-20240115-001",
"callback_url": "https://yourdomain.com/payment/callback",
"customer_name": "Jane Doe",
"customer_email": "jane@example.com",
"title": "Order #001 — Pay by Transfer",
"metadata": { "order_id": 42 }
}'<?php
$ch = curl_init('https://wittypay.online/api/merchant/v1/payments');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 5000,
'reference' => 'TRF-20240115-001',
'callback_url' => 'https://yourdomain.com/payment/callback',
'customer_name' => 'Jane Doe',
'customer_email'=> 'jane@example.com',
'metadata' => ['order_id' => 42],
]),
]);
$res = json_decode(curl_exec($ch), true);
if ($res['status'] === 'success') {
// Redirect customer to hosted checkout — they'll see the bank account number
header('Location: ' . $res['data']['checkout_url']);
exit;
}const res = await fetch('https://wittypay.online/api/merchant/v1/payments', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 5000,
reference: 'TRF-20240115-001',
callback_url: 'https://yourdomain.com/payment/callback',
customer_name: 'Jane Doe',
customer_email:'jane@example.com',
metadata: { order_id: 42 },
}),
});
const data = await res.json();
// Redirect customer to the hosted checkout
window.location.href = data.data.checkout_url;import requests
r = requests.post(
'https://wittypay.online/api/merchant/v1/payments',
json={
'amount': 5000,
'reference': 'TRF-20240115-001',
'callback_url': 'https://yourdomain.com/payment/callback',
'customer_email':'jane@example.com',
},
headers={'Authorization': 'Bearer sk_live_YOUR_SECRET_KEY'},
)
data = r.json()
# Redirect to: data['data']['checkout_url']Success Response (201)
{
"status": "success",
"data": {
"reference": "TRF-20240115-001",
"order_no": "PP20240115123456",
"checkout_url": "https://checkout.palmpay-inc.com/...",
"amount": 5000,
"currency": "NGN",
"status": "processing",
"expires_in": 3600
}
}
Payment Status Values
| Status | Description |
|---|---|
| processing | Payment order created, waiting for customer to make the transfer |
| success | Transfer confirmed — your balance has been credited |
| failed | Payment processing encountered an error |
| refunded | Payment was refunded to the customer |
| expired | Payment window timed out with no transfer received |
status === "success" and pay_method === "BANK_TRANSFER" before fulfilling the order.curl "https://wittypay.online/api/merchant/v1/payments?ref=TRF-20240115-001" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
Response
{
"status": "success",
"data": {
"reference": "TRF-20240115-001",
"order_no": "PP20240115123456",
"status": "success",
"amount": 5000,
"fee": 75,
"net_amount": 4925,
"currency": "NGN",
"pay_method": "BANK_TRANSFER",
"customer": {
"name": "Jane Doe",
"email": "jane@example.com"
}
}
}
Callback Notification
When the customer completes the bank transfer, Wittypay sends a POST request to your callback_url with the payment details. The request is signed — always verify the signature before processing.
payment.success event to both your per-request callback_url and your dashboard-configured Webhook URL. Your server is notified even if one URL is temporarily inaccessible.Callback Headers
| Header | Description |
|---|---|
| X-Wittypay-Signature | sha512=<HMAC-SHA512 signature> — use this to verify authenticity |
| X-Wittypay-Event | Event name, e.g. payment.success |
| Content-Type | application/json |
Callback Body
{
"event": "payment.success",
"timestamp": "2024-01-15T10:31:00+01:00",
"data": {
"reference": "TRF-20240115-001",
"orderNo": "PP20240115123456",
"amount": 5000,
"fee": 75,
"net": 4925,
"currency": "NGN",
"status": "success",
"pay_method": "BANK_TRANSFER",
"metadata": { "order_id": 42 }
}
}
Verifying the Callback Signature
The X-Wittypay-Signature header is sha512=HMAC-SHA512(raw_body, secret_key). Always verify before acting on the callback.
<?php
$SECRET_KEY = 'sk_live_YOUR_SECRET_KEY';
$rawBody = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_WITTYPAY_SIGNATURE'] ?? '';
// Strip "sha512=" prefix and verify
$received = str_replace('sha512=', '', $sigHeader);
$expected = hash_hmac('sha512', $rawBody, $SECRET_KEY);
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true);
$event = $payload['event'] ?? '';
$data = $payload['data'] ?? [];
// Respond 200 first to stop retries, then process
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);
flush();
if ($event === 'payment.success') {
$ref = $data['reference'];
$amount = $data['amount'];
// Always verify server-side before fulfilling
$ch = curl_init('https://wittypay.online/api/merchant/v1/payments?ref=' . $ref);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_live_YOUR_SECRET_KEY'],
]);
$verified = json_decode(curl_exec($ch), true);
if (($verified['data']['status'] ?? '') === 'success') {
fulfillOrder($ref); // your business logic here
}
}const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sigHeader = req.headers['x-wittypay-signature'] || '';
const received = sigHeader.replace('sha512=', '');
const expected = crypto
.createHmac('sha512', 'sk_live_YOUR_SECRET_KEY')
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(received)
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
res.json({ received: true });
const payload = JSON.parse(req.body);
if (payload.event === 'payment.success') {
fulfillOrder(payload.data.reference);
}
});import hmac, hashlib
from flask import request, jsonify
@app.route('/webhook', methods=['POST'])
def webhook():
raw_body = request.get_data()
sig_hdr = request.headers.get('X-Wittypay-Signature', '')
received = sig_hdr.replace('sha512=', '')
expected = hmac.new(
b'sk_live_YOUR_SECRET_KEY', raw_body, hashlib.sha512
).hexdigest()
if not hmac.compare_digest(expected, received):
return jsonify({'error': 'Invalid signature'}), 401
payload = request.json
if payload['event'] == 'payment.success':
fulfill_order(payload['data']['reference'])
return jsonify({'received': True})Important Notes
- Always verify the
X-Wittypay-Signatureheader before processing any callback - Return HTTP 200 immediately to acknowledge receipt — Wittypay retries on non-2xx responses
- Use your
referencefield to match callbacks to your orders - Only process payments where
status === "success" - Deduplicate by
referenceto guard against duplicate deliveries - Always call
GET /payments?ref=REFERENCEto confirm status server-side before fulfilling
Virtual Accounts
Issue dedicated Nigerian bank account numbers for your customers. When a customer transfers money to the account, your Wittypay balance is credited and a virtual_account.credit webhook fires. Supports permanent (reusable) and one-time accounts.
identity_type, license_number, and customer_name are required by our processor for KYC compliance. Use BVN or NIN for individuals, RC for registered companies, and TIN for tax entities.| Parameter | Type | Description | Required |
|---|---|---|---|
| account_name | string | Label displayed on the virtual account (e.g. your business name) | Required |
| identity_type | string | KYC type: BVN, NIN, RC, or TIN | Required |
| license_number | string | ID number matching identity_type (11-digit BVN/NIN, CAC RC number, etc.) | Required |
| customer_name | string | Full legal name matching the identity document | Required |
| customer_email | string | Customer's email address | Optional |
| reference | string | Your unique reference. Auto-generated if omitted. | Optional |
| description | string | Purpose or label for this account | Optional |
| is_permanent | boolean | Keep active after first credit (default: true). Set false for one-time top-ups. | Optional |
curl -X POST "https://wittypay.online/api/merchant/v1/virtual-accounts" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_name": "WITTY TECH LTD",
"identity_type": "RC",
"license_number": "1234567",
"customer_name": "Witty Tech Limited",
"customer_email": "payments@example.com",
"description": "Customer wallet top-up",
"is_permanent": true
}'<?php
$ch = curl_init('https://wittypay.online/api/merchant/v1/virtual-accounts');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'account_name' => 'WITTY TECH LTD',
'identity_type' => 'RC',
'license_number' => '1234567',
'customer_name' => 'Witty Tech Limited',
'customer_email' => 'payments@example.com',
'description' => 'Customer wallet top-up',
'is_permanent' => true,
]),
]);
$res = json_decode(curl_exec($ch), true);const res = await fetch('https://wittypay.online/api/merchant/v1/virtual-accounts', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
account_name: 'WITTY TECH LTD',
identity_type: 'RC',
license_number: '1234567',
customer_name: 'Witty Tech Limited',
customer_email: 'payments@example.com',
is_permanent: true,
}),
});
const data = await res.json();Success Response (201)
{
"status": "success",
"data": {
"id": 12,
"account_name": "WITTY TECH LTD",
"account_number": "8012345678",
"bank_name": "PalmPay",
"bank_code": "999991",
"reference": "VAR20240115ABCDEF",
"description": "Customer wallet top-up",
"is_permanent": true,
"status": "active",
"total_received": 0,
"created_at": "2024-01-15 10:30:00"
}
}
| Query Parameter | Type | Description |
|---|---|---|
| id | integer | Return a single VA by ID |
| account_no | string | Return a single VA by account number |
| status | string | Filter: active · inactive · frozen · expired |
| page | integer | Page number (default: 1) |
| per_page | integer | Results per page, max 100 (default: 20) |
# List all active VAs
curl "https://wittypay.online/api/merchant/v1/virtual-accounts?status=active&page=1" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
# Get single VA by ID
curl "https://wittypay.online/api/merchant/v1/virtual-accounts?id=12" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
Payouts
Programmatically send money from your Wittypay balance to any Nigerian bank account. A flat fee of ₦53.75 is charged per payout.
GET /banks before initiating.| Parameter | Type | Description | Required |
|---|---|---|---|
| amount | number | Amount in NGN (min ₦100). Fee of ₦53.75 is added on top. | Required |
| reference | string | Your unique payout reference | Required |
| bank_code | string | Destination bank code (use GET /banks to list) | Required |
| account_number | string | 10-digit NUBAN account number | Required |
| account_name | string | Recipient's name (verify first with GET /banks) | Optional |
| narration | string | Transfer narration on recipient's bank statement | Optional |
curl -X POST "https://wittypay.online/api/merchant/v1/payouts" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 10000,
"reference": "PAY-20240115-001",
"bank_code": "044",
"account_number": "0123456789",
"account_name": "John Smith",
"narration": "Vendor payment Jan 2024"
}'<?php
$ch = curl_init('https://wittypay.online/api/merchant/v1/payouts');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 10000,
'reference' => 'PAY-20240115-001',
'bank_code' => '044',
'account_number' => '0123456789',
'account_name' => 'John Smith',
'narration' => 'Vendor payment',
]),
]);
$res = json_decode(curl_exec($ch), true);const res = await fetch('https://wittypay.online/api/merchant/v1/payouts', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_YOUR_SECRET_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 10000,
reference: 'PAY-20240115-001',
bank_code: '044',
account_number: '0123456789',
account_name: 'John Smith',
narration: 'Vendor payment',
}),
});
const data = await res.json();Success Response (201)
{
"status": "success",
"data": {
"reference": "PAY-20240115-001",
"order_no": "PPO20240115789012",
"status": "processing",
"amount": 10000,
"fee": 53.75,
"currency": "NGN"
}
}
curl "https://wittypay.online/api/merchant/v1/payouts?ref=PAY-20240115-001" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
Response
{
"status": "success",
"data": {
"reference": "PAY-20240115-001",
"gateway_ref": "PPO20240115789012",
"status": "success",
"amount": 10000,
"fee": 53.75,
"recipient_bank": "Access Bank",
"recipient_account": "0123456789",
"recipient_name": "John Smith",
"narration": "Vendor payment Jan 2024",
"created_at": "2024-01-15 10:30:00",
"processed_at": "2024-01-15 10:30:18"
}
}
Balance
Retrieve your current available and ledger balance, pending amounts, and 30-day transaction summary.
curl "https://wittypay.online/api/merchant/v1/balance" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
Response
{
"status": "success",
"data": {
"currency": "NGN",
"available": 248500.00,
"ledger": 248500.00,
"pending": 5000.00,
"total_volume": 1284000.00,
"total_count": 214,
"last_30d_volume": 92000.00,
"last_30d_count": 38
}
}
Transactions
List and filter your full payment history with pagination.
| Query Parameter | Type | Description |
|---|---|---|
| reference | string | Filter by your reference |
| status | string | success · failed · pending · processing · refunded |
| type | string | payment · payout · refund |
| from | string | Start date YYYY-MM-DD |
| to | string | End date YYYY-MM-DD |
| page | integer | Page number (default: 1) |
| per_page | integer | Max 100 (default: 20) |
curl "https://wittypay.online/api/merchant/v1/transactions?status=success&from=2024-01-01&per_page=50" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
Response
{
"status": "success",
"data": [ /* array of transaction objects */ ],
"meta": {
"total": 214, "page": 1, "per_page": 50, "pages": 5
}
}
Customers
Store customer profiles for CRM, reconciliation, and repeat-payment flows. Creating a customer with a duplicate email/phone updates their existing record (upsert).
| Query Parameter | Type | Description |
|---|---|---|
| string | Exact email match | |
| phone | string | Exact phone match |
| code | string | Customer code (e.g. CUS0A1B2C3D4E) |
| page | integer | Page (default: 1) |
| per_page | integer | Max 100 (default: 20) |
| Parameter | Type | Description | Required |
|---|---|---|---|
| first_name | string | First name | Optional |
| last_name | string | Last name | Optional |
| string | Email address | Optional | |
| phone | string | Phone number | Optional |
| metadata | object | Custom key-value data | Optional |
Bank Codes
Use these codes in the bank_code field when initiating payouts. You can also call GET /banks to resolve account names before sending money.
| Query Parameter | Description |
|---|---|
| account_number | 10-digit account number — returns resolved account name |
| bank_code | Bank code — required with account_number for name enquiry |
# List all banks
curl "https://wittypay.online/api/merchant/v1/banks" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
# Verify account name before payout
curl "https://wittypay.online/api/merchant/v1/banks?account_number=0123456789&bank_code=044" \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY"
Name Enquiry Response
{
"status": "success",
"account_name": "JOHN SMITH",
"account_no": "0123456789",
"bank_code": "044",
"bank_name": "Access Bank"
}
| Bank Name | Code |
|---|---|
| Access Bank | 044 |
| Citibank Nigeria | 023 |
| Ecobank Nigeria | 050 |
| Fidelity Bank | 070 |
| First Bank of Nigeria | 011 |
| First City Monument Bank (FCMB) | 214 |
| Globus Bank | 103 |
| Guaranty Trust Bank (GTBank) | 058 |
| Heritage Bank | 030 |
| Keystone Bank | 082 |
| Kuda Microfinance Bank | 90267 |
| Lotus Bank | 303 |
| Moniepoint Microfinance Bank | 50515 |
| OPay Digital Services (OPay) | 999992 |
| PalmPay | 999991 |
| Parallex Bank | 526 |
| Polaris Bank | 076 |
| Providus Bank | 101 |
| Stanbic IBTC Bank | 221 |
| Standard Chartered Bank | 068 |
| Sterling Bank | 232 |
| SunTrust Bank | 100 |
| Titan Trust Bank | 102 |
| Union Bank of Nigeria | 032 |
| United Bank for Africa (UBA) | 033 |
| Unity Bank | 215 |
| VFD Microfinance Bank | 566 |
| Wema Bank | 035 |
| Zenith Bank | 057 |
Call GET /banks to get the full live list of supported banks and their codes.
Webhooks
Wittypay uses webhooks to notify your server of real-time events — payment completions, virtual account credits, payout outcomes. Configure your webhook URL in the dashboard → Settings → Webhooks.
Webhook Events
| Event | Trigger |
|---|---|
payment.success | Customer payment (checkout or bank transfer) confirmed successfully |
payment.failed | Payment could not be processed or expired |
virtual_account.credit | Money received into a virtual account |
payout.success | Payout transfer delivered to recipient's bank |
payout.failed | Payout rejected or reversed by recipient's bank |
Event Payloads
{
"event": "payment.success",
"timestamp": "2024-01-15T10:31:00+01:00",
"data": {
"reference": "ORDER-001",
"orderNo": "PP20240115123456",
"amount": 5000,
"fee": 75,
"net": 4925,
"currency": "NGN",
"status": "success",
"pay_method": "BANK_TRANSFER",
"customer": { "name": "Jane Doe", "email": "jane@example.com" },
"metadata": { "order_id": 42 }
}
}
{
"event": "virtual_account.credit",
"timestamp": "2024-01-15T11:00:05+01:00",
"data": {
"virtual_account_id": 12,
"account_number": "8012345678",
"account_name": "WITTY TECH LTD",
"amount": 25000,
"sender_name": "EMEKA OKAFOR",
"sender_bank": "GTBank",
"reference": "VAR20240115ABCDEF",
"session_id": "000014240115100005000000001234",
"currency": "NGN"
}
}
Complete Webhook Handler
<?php
// webhook.php — place at a public URL and register in your dashboard
$SECRET = 'sk_live_YOUR_SECRET_KEY';
$rawBody = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_WITTYPAY_SIGNATURE'] ?? '';
if (!str_starts_with($sigHeader, 'sha512=')) {
http_response_code(401); exit('Missing signature');
}
$received = substr($sigHeader, 7);
$expected = hash_hmac('sha512', $rawBody, $SECRET);
if (!hash_equals($expected, $received)) {
http_response_code(401); exit('Invalid signature');
}
// Acknowledge immediately
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true]);
flush();
$payload = json_decode($rawBody, true);
$event = $payload['event'] ?? '';
$data = $payload['data'] ?? [];
switch ($event) {
case 'payment.success':
handlePaymentSuccess($data);
break;
case 'virtual_account.credit':
handleVACredit($data);
break;
case 'payout.success':
handlePayoutSuccess($data);
break;
case 'payout.failed':
handlePayoutFailed($data);
break;
}
function handlePaymentSuccess($data): void {
$ref = $data['reference'];
$amount = $data['amount'];
// TODO: verify server-side, then fulfil order for $ref
}const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = 'sk_live_YOUR_SECRET_KEY';
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = (req.headers['x-wittypay-signature'] || '').replace('sha512=', '');
const expected = crypto.createHmac('sha512', SECRET).update(req.body).digest('hex');
try {
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)))
return res.status(401).json({ error: 'Invalid signature' });
} catch { return res.status(401).json({ error: 'Bad signature' }); }
res.json({ received: true });
const { event, data } = JSON.parse(req.body);
switch (event) {
case 'payment.success': handlePayment(data); break;
case 'virtual_account.credit': handleVACredit(data); break;
case 'payout.success': handlePayoutOk(data); break;
case 'payout.failed': handlePayoutFailed(data); break;
}
});Test Mode
Test your integration without moving real money. Test mode uses a separate set of API keys — find them in the dashboard → Settings → API Keys.
sk_test_. Live keys start with sk_live_. Never use live keys in development or client-side code.Test Mode
- ✓ No real money moved
- ✓ Webhooks fire to your server
- ✓ Use test card numbers below
- ✓ Safe for development & QA
Live Mode
- ✓ Real transactions
- ✓ Real funds settled
- ✗ Requires completed KYC
- ✗ No test card support
Test Credentials
| Scenario | Test Value |
|---|---|
| Successful bank transfer | Complete the hosted checkout with any amount |
| Failed payment | Use amount 0.01 (below minimum) |
| Test BVN (for Virtual Accounts) | 22222222222 |
| Test NIN | 12345678901 |
| Test account number | 0000000000 with any bank code |
Security Best Practices
Follow these guidelines to protect your integration and your customers.
Keep API Keys Secret
Never embed secret keys in client-side code, mobile apps, or public repositories. Use environment variables and a secrets manager. Rotate keys immediately if compromised.
Verify Every Webhook
Always validate the X-Wittypay-Signature header using hash_equals / timingSafeEqual. Never process a webhook without verifying its signature — even if it looks legitimate.
Confirm Server-Side
After receiving a callback or webhook, always call GET /payments?ref=REFERENCE to verify status server-side before fulfilling orders. Callback data alone should never trigger irreversible actions.
Deduplicate Events
Wittypay may deliver the same event more than once. Track processed reference IDs in your database and use INSERT IGNORE or an idempotency check to avoid double-fulfilling orders.
Use HTTPS Everywhere
Your callback_url and webhook endpoint must use HTTPS. All Wittypay API calls already use HTTPS. Never send API keys or payment data over an unencrypted connection.
Validate Amounts
Before fulfilling an order, compare the amount in the webhook/API response against what you charged. Reject payments where the amount doesn't match to prevent customers from paying less than owed.