Wittypay API

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.

REST / JSON HTTPS only Bearer Auth Webhooks Get API Keys →

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.

PropertyValue
Base URLhttps://wittypay.online/api/merchant/v1
ProtocolHTTPS only
Data FormatJSON (request body & responses)
AuthenticationAuthorization: Bearer <secret_key>
CurrencyNigerian Naira (NGN). All amounts in Naira — e.g. 5000 = ₦5,000
EncodingUTF-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.

Generate your API key from the API Keys page. Live keys are prefixed sk_live_. Sandbox keys are prefixed sk_test_.

Required Headers

HeaderDescriptionRequired
AuthorizationBearer YOUR_SECRET_KEYRequired
Content-Typeapplication/jsonRequired
# 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 StatusMeaning
200Success
201Created — payment order or payout initiated
400Bad Request — missing or invalid field
401Unauthorized — missing or invalid API key
403Forbidden — key disabled or account not approved
404Not Found — resource does not exist for your account
405Method Not Allowed
409Conflict — duplicate reference already exists
422Unprocessable — account could not be verified
500Internal Server Error
502Bad 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.

POST /payments Create a payment order
ParameterTypeDescriptionRequired
amountnumberAmount in NGN (minimum ₦100)Required
referencestringYour unique transaction referenceRequired
callback_urlstringURL customer is redirected to after paymentRequired
customer_namestringCustomer's full nameOptional
customer_emailstringCustomer's email addressOptional
customer_phonestringCustomer's phone numberOptional
titlestringPayment title shown on checkout (default: Payment)Optional
descriptionstringAdditional payment descriptionOptional
notify_urlstringOverride your account webhook URL for this order onlyOptional
metadataobjectCustom key-value data returned in webhookOptional
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_url
import 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
  }
}

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

1

Initiate the payment

Your server calls POST /payments with the amount, customer info, and your callback_url.

2

Get the checkout URL

Wittypay returns a checkout_url pointing to a hosted payment page.

3

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.

4

Customer makes the transfer

The customer transfers the exact amount from any Nigerian bank to the provided account number.

5

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.

6

Verify and fulfil

Your server verifies the webhook signature, calls GET /payments?ref=REFERENCE to confirm, then fulfils the order.

Easiest Integration: Just redirect your customer to the checkout_url returned in the response. Wittypay handles the entire payment UI, countdown timer, and status updates — no extra frontend code needed.
POST /payments Initiate a bank transfer payment
ParameterTypeDescriptionRequired
amountnumberAmount in NGN (min ₦100, max ₦10,000,000)Required
referencestringYour unique reference for this payment (e.g. order ID)Required
callback_urlstringURL to redirect customer after payment and receive webhookRequired
customer_namestringCustomer's full nameOptional
customer_emailstringCustomer's email addressOptional
customer_phonestringCustomer's phone numberOptional
titlestringPayment title shown on checkoutOptional
descriptionstringPayment description (max 500 chars)Optional
metadataobjectCustom key-value data returned in webhookOptional
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
  }
}
Hosted Checkout Recommended

The easiest way to accept bank transfer payments — redirect your customer to the checkout_url. The hosted checkout page includes:

  • Bank account details with one-click copy button
  • Real-time countdown timer with expiry warning
  • Automatic payment detection — auto-redirects on completion
  • USSD code and QR code alternative payment methods
  • Fully responsive, mobile-optimised UI
// After calling POST /payments — redirect customer to hosted checkout
const result = await response.json();
window.location.href = result.data.checkout_url;

Payment Status Values

StatusDescription
processingPayment order created, waiting for customer to make the transfer
successTransfer confirmed — your balance has been credited
failedPayment processing encountered an error
refundedPayment was refunded to the customer
expiredPayment window timed out with no transfer received

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.

Dual Notification: Wittypay sends the 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

HeaderDescription
X-Wittypay-Signaturesha512=<HMAC-SHA512 signature> — use this to verify authenticity
X-Wittypay-EventEvent name, e.g. payment.success
Content-Typeapplication/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

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.

The fields 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.
POST /virtual-accounts Create a virtual account
ParameterTypeDescriptionRequired
account_namestringLabel displayed on the virtual account (e.g. your business name)Required
identity_typestringKYC type: BVN, NIN, RC, or TINRequired
license_numberstringID number matching identity_type (11-digit BVN/NIN, CAC RC number, etc.)Required
customer_namestringFull legal name matching the identity documentRequired
customer_emailstringCustomer's email addressOptional
referencestringYour unique reference. Auto-generated if omitted.Optional
descriptionstringPurpose or label for this accountOptional
is_permanentbooleanKeep 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"
  }
}

Payouts

Programmatically send money from your Wittypay balance to any Nigerian bank account. A flat fee of ₦53.75 is charged per payout.

Payout funds are deducted from your balance at initiation. If the transfer is rejected by the bank, your balance is automatically refunded. Always verify bank account details with GET /banks before initiating.
POST /payouts Initiate a payout
ParameterTypeDescriptionRequired
amountnumberAmount in NGN (min ₦100). Fee of ₦53.75 is added on top.Required
referencestringYour unique payout referenceRequired
bank_codestringDestination bank code (use GET /banks to list)Required
account_numberstring10-digit NUBAN account numberRequired
account_namestringRecipient's name (verify first with GET /banks)Optional
narrationstringTransfer narration on recipient's bank statementOptional
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"
  }
}

Balance

Retrieve your current available and ledger balance, pending amounts, and 30-day transaction summary.

GET /balance Get account balance
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.

GET /transactions List transactions
Query ParameterTypeDescription
referencestringFilter by your reference
statusstringsuccess · failed · pending · processing · refunded
typestringpayment · payout · refund
fromstringStart date YYYY-MM-DD
tostringEnd date YYYY-MM-DD
pageintegerPage number (default: 1)
per_pageintegerMax 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).

GET /customers List or search customers
Query ParameterTypeDescription
emailstringExact email match
phonestringExact phone match
codestringCustomer code (e.g. CUS0A1B2C3D4E)
pageintegerPage (default: 1)
per_pageintegerMax 100 (default: 20)

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.

Bank NameCode
Access Bank044
Citibank Nigeria023
Ecobank Nigeria050
Fidelity Bank070
First Bank of Nigeria011
First City Monument Bank (FCMB)214
Globus Bank103
Guaranty Trust Bank (GTBank)058
Heritage Bank030
Keystone Bank082
Kuda Microfinance Bank90267
Lotus Bank303
Moniepoint Microfinance Bank50515
OPay Digital Services (OPay)999992
PalmPay999991
Parallex Bank526
Polaris Bank076
Providus Bank101
Stanbic IBTC Bank221
Standard Chartered Bank068
Sterling Bank232
SunTrust Bank100
Titan Trust Bank102
Union Bank of Nigeria032
United Bank for Africa (UBA)033
Unity Bank215
VFD Microfinance Bank566
Wema Bank035
Zenith Bank057

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.

Retry Policy: If your endpoint doesn't respond with HTTP 2xx within 30 seconds, Wittypay retries up to 5 times at 5-minute intervals. Always respond 200 immediately, then process asynchronously.

Webhook Events

EventTrigger
payment.successCustomer payment (checkout or bank transfer) confirmed successfully
payment.failedPayment could not be processed or expired
virtual_account.creditMoney received into a virtual account
payout.successPayout transfer delivered to recipient's bank
payout.failedPayout rejected or reversed by recipient's bank

Event Payloads

payment.success Fired when a payment is confirmed
{
  "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 }
  }
}

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.

Test keys start with 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

ScenarioTest Value
Successful bank transferComplete the hosted checkout with any amount
Failed paymentUse amount 0.01 (below minimum)
Test BVN (for Virtual Accounts)22222222222
Test NIN12345678901
Test account number0000000000 with any bank code
Webhook Testing: Use webhook.site or smee.io to inspect webhook payloads during local development, then set your real URL in the dashboard before going live.

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.