API reference

ClassyPay merchant API

Accept Naira bank-transfer payments with a hosted checkout. Your server creates a checkout, you send the customer to the payment page (or show the account details yourself), and ClassyPay confirms the payment automatically when the transfer arrives, credits your wallet and notifies your webhook.

Base URL
https://dclassypay.com
Format
JSON or form-encoded requests, JSON responses
Currency
NGN (amounts in Naira with up to 2 decimals)
Timestamps
ISO 8601, UTC

Authentication

Create a secret key under Dashboard → API keys once your business verification is approved. It is shown once; store it on your server only and never in a browser or mobile app.

Header
Authorization: Bearer cp_live_xxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Several businesses? Each key belongs to one business (pick it with the business switcher before creating the key). Checkouts and customer accounts created with a key are recorded under that business, its customers see its name on the payment page, and its events go to that business's webhook endpoint. A key only sees its own business's checkouts and customer accounts.

Create a checkout

POST/api/checkout/create
FieldRequiredDescription
referenceYesYour unique id for this payment. Up to 80 letters, digits and . _ : -. Each reference can be used once.
amountYesNaira as a string or number with at most two decimals, e.g. "2500.00".
customer_emailYesThe payer's email address.
currencyNoNGN, the only supported currency.
success_urlNoWhere the payment page sends the customer after paying.
cancel_urlNoWhere the payment page sends the customer if the checkout expires or is cancelled.
RequestPOST
curl https://dclassypay.com/api/checkout/create \
  -H "Authorization: Bearer $CLASSYPAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reference":"order_1042","amount":"25000.00","customer_email":"[email protected]"}'
Response · 201 Created
{
  "data": {
    "reference": "order_1042",
    "checkout_id": "9f1c2b7e4a5d4c3b8a7f6e5d4c3b2a19",
    "checkout_url": "https://dclassypay.com/checkout/?id=9f1c2b7e4a5d4c3b8a7f6e5d4c3b2a19",
    "status": "awaiting_payment",
    "amount": "25000.00",
    "fee": null,
    "net_amount": null,
    "currency": "NGN",
    "customer_email": "[email protected]",
    "expires_at": "2026-10-03T15:30:00Z",
    "paid_at": null,
    "created_at": "2026-10-03T15:00:00Z",
    "bank_transfer": {
      "bank_name": "…",
      "account_number": "…",
      "account_name": "ClassyPay CHECKOUT C457"
    }
  }
}

Redirect the customer to checkout_url, or show the bank_transfer details in your own page. The account is reserved for this checkout until expires_at; the customer must transfer the exact amount before then.

Get a checkout

GET/api/checkout/status?reference=order_1042

Returns the same object. status is one of awaiting_payment, paid, completed, expired or cancelled. Treat completed as final success: your wallet has been credited and fee and net_amount are set.

Cancel a checkout

POST/api/checkout/cancel
Body
{ "reference": "order_1042" }

Only checkouts that are still awaiting payment can be cancelled; otherwise the API answers 409 conflict.

Permanent customer accounts

Give each of your customers their own permanent account number. They can save it and transfer any amount at any time; every transfer is credited to your wallet automatically (minus the ClassyPay fee) and announced with a customer_account.credited webhook. Available once enabled for your business.

POST/api/customer-accounts/create
FieldRequiredDescription
customer_idYesYour own id for the customer (up to 80 letters, digits, . _ : -). Returned on every payment.
customer_nameYesThe customer's real name.
customer_emailYesThe customer's email address. Required to issue a new account number; our bank partner sends payment receipts to it. Requests without it return 422 invalid_request.

Idempotent: 201 for a new account, 200 with the same account when the customer_id already exists. Safe to call again after a timeout or a 503. Accounts already issued keep working whether or not they have an email. If an account was not issued (status failed), call create again with the same customer_id and a customer_email: you get the same account, never a duplicate.

Response
{
  "data": {
    "customer_id": "user_8841",
    "customer_name": "Ngozi Nwosu",
    "customer_email": "[email protected]",
    "status": "active",
    "total_received": "0.00",
    "payments_count": 0,
    "last_payment_at": null,
    "created_at": "2026-10-07T09:00:00Z",
    "bank_transfer": { "bank_name": "…", "account_number": "8021234567", "account_name": "ClassyPay - Ngozi Nwosu" }
  }
}
GET/api/customer-accounts/get?customer_id=user_8841
GET/api/customer-accounts/list?limit=50&offset=0&status=active
GET/api/customer-accounts/payments?customer_id=user_8841
POST/api/customer-accounts/status  {"customer_id": "user_8841", "status": "disabled"}

A disabled account still exists at the bank, so transfers can still arrive; they are held for our team to review instead of being credited automatically.

Errors

The HTTP status is the answer and the body names what to fix. Branch on type; show message to developers, not customers.

Error body
{ "error": { "type": "invalid_request", "message": "amount must be at least ₦100.00." } }
HTTPtypeMeaning
401unauthorizedMissing, revoked or invalid API key.
403account_inactiveYour merchant account is suspended. Contact support.
403not_verifiedBusiness verification is not approved yet.
403business_inactiveThe business this key belongs to is awaiting review, was not approved, or is suspended.
404not_foundNo checkout with that reference on your account.
409conflictThe checkout is not in a state that allows this action.
422invalid_requestA field is missing or invalid; the message says which.
503unavailableNo checkout account is free right now. Retry shortly with the same reference.
500server_errorUnexpected error. Retry later and quote the Ref in the message to support.

Webhooks

Set a public HTTPS endpoint under Dashboard → Webhooks. The signing secret is shown once when you save or rotate it.

EventWhen
checkout.paidThe transfer for the exact amount was received.
checkout.completedThe payment was credited to your wallet. Fulfil the order on this event.
checkout.expiredThe checkout expired without payment.
checkout.cancelledThe checkout was cancelled.
withdrawal.pending / withdrawal.awaiting_approvalA withdrawal was requested (large ones wait for a quick review).
customer_account.creditedA transfer into a customer account was credited. data has customer_id, account_number, amount, fee, net_amount, payer_name, payment_reference, paid_at.
withdrawal.successThe withdrawal was paid to your bank account.
withdrawal.failed / withdrawal.reversed / withdrawal.rejected / withdrawal.cancelledThe withdrawal did not complete; amount and fee were returned to your wallet.
Request we send
POST <your url>
Content-Type: application/json
X-ClassyPay-Event: checkout.completed
X-ClassyPay-Delivery: cpwd_418
X-ClassyPay-Signature: <hex HMAC-SHA512 of the raw body>

{
  "id": "evt_3f0c…",
  "event": "checkout.completed",
  "created_at": "2026-10-03T15:04:11+00:00",
  "data": {
    "reference": "order_1042",
    "checkout_id": "9f1c2b7e4a5d4c3b8a7f6e5d4c3b2a19",
    "status": "completed",
    "amount": "25000.00",
    "fee": "375.00",
    "net_amount": "24625.00",
    "currency": "NGN",
    "customer_email": "[email protected]",
    "payment_reference": "20261003150411236399951256"
  }
}

Return any 2xx within 12 seconds. Failed deliveries are retried after roughly 30s, 2m, 10m, 30m, 1h, 3h and 12h (8 attempts). You can see every attempt and resend a delivery from the dashboard. Paused endpoints keep their events until you resume.

Verify signatures

Always verify the signature over the raw request body before parsing it, and use id as an idempotency key because an event can be delivered more than once.

PHP
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha512', $raw, getenv('CLASSYPAY_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_CLASSYPAY_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);
// Fulfil the order for $event['data']['reference'] once per $event['id'].
http_response_code(200);

Fees

ClassyPay charges 2.5% per successful payment. The fee is deducted before your wallet is credited and appears on the payment, in your ledger, and in the fee and net_amount fields of the API and webhooks.

Questions? Email [email protected].