Virtual accounts
Give a customer a unique bank account to pay into.
Contents
How virtual accounts work
A virtual account is a real, bank-addressable NUBAN that exists for one purpose: to receive one specific payment from one specific customer. Because the account number is unique to that payment, a transfer into it is unambiguous — there is no reference field for the customer to mistype and no reconciliation to do.
- You mint an account for an exact amount.
- Your customer transfers that amount from any Nigerian bank.
- Radok matches the inflow, credits your balance net of fees, marks the linked collection
succeeded, and closes the account. - You receive a
collection.successwebhook.
One account per payment, not per customer
Create a virtual account
/v1/virtual-accounts- Auth
- Secret key
- Scope
- virtual_accounts:write
API key only. Minting is a billable PSP write, so it is not exposed to a dashboard session.
Body
| Field | Type | Description |
|---|---|---|
| customerEmailrequired | string | The payer's email. Used to identify the collection and in receipts. |
| customerName | string | Up to 120 characters. Shown to the payer in the account name where the bank allows it. |
| amountrequired | object | { "value": integer, "currency": "NGN" } in kobo. Must be positive. The account expects this exact amount. |
| expiresInMinutes | integer | 1–1440. When set, Radok expires the collection after this many minutes, emits collection.failed with reason: "expired", and stops accepting payment. Omit it and no expiry job is scheduled — pass a value whenever you need the collection to time out. |
curl -X POST https://api.radokpay.com/v1/virtual-accounts \
-H "Authorization: Bearer $RADOK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerEmail": "ada@example.com",
"customerName": "Ada Lovelace",
"amount": { "value": 500000, "currency": "NGN" },
"expiresInMinutes": 60
}'{
"data": {
"reference": "va_9f4c1b8e2d7a6c05",
"bankName": "Providus Bank",
"accountNumber": "9901234567",
"accountName": "RADOK/ADA LOVELACE",
"expiresAt": "2026-08-23T12:30:00.000Z",
"status": "active",
"collection": {
"reference": "col_3a7d5e1f9b2c4d68",
"amount": { "value": 500000, "currency": "NGN" },
"status": "awaiting_payment"
}
}
}Response
| Field | Type | Description |
|---|---|---|
| reference | string | Virtual account reference, prefixed va_. |
| bankName | string | Show this to the payer alongside the number. |
| accountNumber | string | The 10-digit NUBAN to transfer into. |
| accountName | string | The name the payer's banking app will display. |
| expiresAt | string | null | ISO 8601. After this the account stops accepting payment. |
| status | string | active, expired, or closed. |
| collection | object | The collection this account was minted for. Track payment status against collection.reference, not the account reference. |
Show the exact amount
Live minting needs KYB
403 KYB_LIVE_REQUIRED until business verification is approved, and amounts above your collection limit return 400 COLLECTION_LIMIT_EXCEEDED. Test mode has neither gate.List virtual accounts
/v1/virtual-accounts- Auth
- Secret key or dashboard session
- Scope
- virtual_accounts:read
Query
| Field | Type | Description |
|---|---|---|
| limit | integer | 1–100. Defaults to 20. |
| cursor | string | From the previous response’s meta.nextCursor. |
| environment | string | test or live. Required only for dashboard sessions — an API key already carries its environment. |
curl "https://api.radokpay.com/v1/virtual-accounts?limit=2" \
-H "Authorization: Bearer $RADOK_SECRET_KEY"{
"data": [
{
"reference": "va_9f4c1b8e2d7a6c05",
"purpose": "collection",
"bankName": "Providus Bank",
"accountNumber": "9901234567",
"accountName": "RADOK/ADA LOVELACE",
"customerEmail": "ada@example.com",
"amountExpected": { "value": 500000, "currency": "NGN" },
"status": "active",
"expiresAt": "2026-08-23T12:30:00.000Z",
"createdAt": "2026-08-23T11:30:00.000Z"
}
],
"meta": { "nextCursor": "652f1a9c4d7b3e0812345678", "hasMore": true }
}Retrieve a virtual account
/v1/virtual-accounts/{reference}- Auth
- Secret key or dashboard session
- Scope
- virtual_accounts:read
curl https://api.radokpay.com/v1/virtual-accounts/va_9f4c1b8e2d7a6c05 \
-H "Authorization: Bearer $RADOK_SECRET_KEY"Returns the same object as a list row. A reference belonging to another merchant returns 404.
Account purposes
Every virtual account carries a purpose. Only the first is created through this endpoint; the others exist so you can tell them apart in a list.
Purpose
| Field | Type | Description |
|---|---|---|
| collection | one-off | Bound to one amount and one collection. Closed as soon as it is paid. |
| funding | permanent | Your own wallet deposit account. Stays open and reusable; deposits credit your balance and emit wallet.funding.success. |
| payment_link | reusable | Minted automatically for a payment link. Stays open for reusable links until disabled; cancelled after the first success on a one_time link. |
Things that catch people out
- Do not poll for payment. Use the
collection.successwebhook. Polling a list endpoint every few seconds will hit the 300/minute read limit long before it tells you anything a webhook would not have. - The customer may never come back. Fulfil on the webhook, not on a redirect back to your app.
- Expiry is not a cancellation. An expired collection emits
collection.failedwithreason: "expired". Handle it, or orders sit pending forever. - Test accounts never receive real money. Trigger settlement yourself — see Testing.