Katpay API Documentation

Build secure payment solutions with Katpay's comprehensive API. Process payments, manage virtual accounts, and handle payouts with ease.

Overview

Katpay provides a robust API for processing payments in Nigeria. Our API allows you to create virtual accounts, process transactions, and manage payouts securely.

Base URL

https://api.katpay.co/v1

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
JavaScript
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.

POST
/virtual-accounts
Create a new virtual account for a customer

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
Customer identity: Send the customer's 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."] }
}
JavaScript
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).

JSON — 201 Created
{
  "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

POST
https://api.katpay.co/v1/payouts

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

JavaScript
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.

JSON — 202 Accepted (processing)
{
  "success": true,
  "status": "processing",
  "message": "Payout is processing. The final status will be confirmed shortly.",
  "data": {
    "reference": "PAY_123456789",
    "status": "processing"
  }
}
JSON — 400 Payout failed (balance refunded)
{
  "success": false,
  "message": "Payout could not be processed. Your balance has been refunded.",
  "data": {
    "reference": "PAY_123456789",
    "status": "failed"
  }
}
HTTP Meaning
200Payout completed immediately (status: completed)
202Accepted and processing — check GET /v1/payouts/{reference} for the result
400Payout failed and was refunded, or not enough withdrawable balance (the message gives the amount available to withdraw now)
403Payouts are not enabled, or are locked, for your account
409Duplicate reference
422Validation error (see errors)
429Too many requests — payouts are limited to 5 per minute
503Payouts are temporarily paused — try again later

Get Payout Status

GET
https://api.katpay.co/v1/payouts/{reference}
The current state of a payout: 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.
JSON — 200 OK
{
  "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

GET
https://api.katpay.co/v1/balance
Your wallet balance. available is the amount you can withdraw right now — check it before sending a payout.
JSON — 200 OK
{
  "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

  1. Your server calls POST /v1/transfer-payments with amount, customer info, and callback URL
  2. KatPay creates a unique payment order and returns bank account details and a checkout_url
  3. Redirect customer to the checkout_url (KatPay hosted checkout — timer, copy button, auto-detect)
  4. Alternatively, build your own UI using the payment_account details from the response
  5. Customer makes a bank transfer to the provided account
  6. KatPay confirms the payment and sends a signed callback to your callback_url and your dashboard webhook URL
  7. Your server verifies the signature and completes the order
💡 Easiest Integration: Just redirect your customer to the checkout_url returned in the response. KatPay handles the entire payment UI, countdown timer, and status updates — no frontend code needed.
POST
/v1/transfer-payments
Initiate a new pay-with-transfer payment. Creates a unique payment order and returns a bank account for the customer.

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)

JSON Response
{
  "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"
  }
}
JavaScript
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_url when payment completes
  • Fully responsive — mobile-optimized UI your customers can trust
Redirect to Checkout
// 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}

GET
/v1/transfer-payments/{uuid}
Check the status of a transfer payment. Use this to poll for payment completion.

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
/v1/transfer-payments/verify/{merchant_reference}
Verify a transfer payment using your own merchant reference. This is the recommended way to confirm payment status from your server — similar to Paystack's GET /transaction/verify/:reference.

Path Parameters

Parameter Type Description
merchant_reference string Your unique reference passed when initiating the transfer payment
Best Practice: Always verify payment status from your server using this endpoint after receiving a callback — never trust the callback alone.

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.)
POST
/v1/transfer-payments/{uuid}/cancel
Cancel a pending transfer payment. Only works for payments with status pending.
GET
/v1/transfer-payments
List all your transfer payments. Supports filtering by status and merchant_reference.

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.

Dual Notification: KatPay sends the transfer payment event to both your per-request 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

JSON Callback Payload
{
  "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
<?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']);
}
Important Notes
  • Always verify the signature before processing the callback
  • Return HTTP 200 to acknowledge receipt. KatPay retries up to 3 times on non-2xx responses
  • Use merchant_reference to match callbacks to your orders
  • Check the status field — only process completed payments
  • Use X-KatPay-Delivery-Id for idempotency to avoid processing duplicates
  • The metadata object 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.

GET
/api/bank-list
Returns the full list of supported bank codes and bank names for payouts.

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
<?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

JSON
{
  "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
    }
  }
}
Always verify webhook signatures
Validate timestamp to prevent replay attacks
Use HTTPS endpoints only

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.