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.
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
| Field | Required | Description |
|---|---|---|
reference | Yes | Your unique id for this payment. Up to 80 letters, digits and . _ : -. Each reference can be used once. |
amount | Yes | Naira as a string or number with at most two decimals, e.g. "2500.00". |
customer_email | Yes | The payer's email address. |
currency | No | NGN, the only supported currency. |
success_url | No | Where the payment page sends the customer after paying. |
cancel_url | No | Where the payment page sends the customer if the checkout expires or is cancelled. |
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]"}'
{
"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
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
{ "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.
| Field | Required | Description |
|---|---|---|
customer_id | Yes | Your own id for the customer (up to 80 letters, digits, . _ : -). Returned on every payment. |
customer_name | Yes | The customer's real name. |
customer_email | Yes | The 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.
{
"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" }
}
}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": { "type": "invalid_request", "message": "amount must be at least ₦100.00." } }| HTTP | type | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, revoked or invalid API key. |
| 403 | account_inactive | Your merchant account is suspended. Contact support. |
| 403 | not_verified | Business verification is not approved yet. |
| 403 | business_inactive | The business this key belongs to is awaiting review, was not approved, or is suspended. |
| 404 | not_found | No checkout with that reference on your account. |
| 409 | conflict | The checkout is not in a state that allows this action. |
| 422 | invalid_request | A field is missing or invalid; the message says which. |
| 503 | unavailable | No checkout account is free right now. Retry shortly with the same reference. |
| 500 | server_error | Unexpected 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.
| Event | When |
|---|---|
checkout.paid | The transfer for the exact amount was received. |
checkout.completed | The payment was credited to your wallet. Fulfil the order on this event. |
checkout.expired | The checkout expired without payment. |
checkout.cancelled | The checkout was cancelled. |
withdrawal.pending / withdrawal.awaiting_approval | A withdrawal was requested (large ones wait for a quick review). |
customer_account.credited | A transfer into a customer account was credited. data has customer_id, account_number, amount, fee, net_amount, payer_name, payment_reference, paid_at. |
withdrawal.success | The withdrawal was paid to your bank account. |
withdrawal.failed / withdrawal.reversed / withdrawal.rejected / withdrawal.cancelled | The withdrawal did not complete; amount and fee were returned to your wallet. |
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.
$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].