RADOK

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.

  1. You mint an account for an exact amount.
  2. Your customer transfers that amount from any Nigerian bank.
  3. Radok matches the inflow, credits your balance net of fees, marks the linked collection succeeded, and closes the account.
  4. You receive a collection.success webhook.

One account per payment, not per customer

Mint a new account for each payment. A returning customer gets a new number; the previous one is closed as soon as it is paid.

Create a virtual account

POST/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

FieldTypeDescription
customerEmailrequiredstringThe payer's email. Used to identify the collection and in receipts.
customerNamestringUp to 120 characters. Shown to the payer in the account name where the bank allows it.
amountrequiredobject{ "value": integer, "currency": "NGN" } in kobo. Must be positive. The account expects this exact amount.
expiresInMinutesinteger1–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
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
  }'
201 Created
{
  "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

FieldTypeDescription
referencestringVirtual account reference, prefixed va_.
bankNamestringShow this to the payer alongside the number.
accountNumberstringThe 10-digit NUBAN to transfer into.
accountNamestringThe name the payer's banking app will display.
expiresAtstring | nullISO 8601. After this the account stops accepting payment.
statusstringactive, expired, or closed.
collectionobjectThe collection this account was minted for. Track payment status against collection.reference, not the account reference.

Show the exact amount

Tell the payer the figure in kobo terms they can transfer. Settlement for a collection account credits the amount you minted for — always display that number and do not let the payer edit it in your UI.

Live minting needs KYB

In live mode, minting returns 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

GET/v1/virtual-accounts
Auth
Secret key or dashboard session
Scope
virtual_accounts:read

Query

FieldTypeDescription
limitinteger1–100. Defaults to 20.
cursorstringFrom the previous response’s meta.nextCursor.
environmentstringtest or live. Required only for dashboard sessions — an API key already carries its environment.
curl
curl "https://api.radokpay.com/v1/virtual-accounts?limit=2" \
  -H "Authorization: Bearer $RADOK_SECRET_KEY"
200 OK
{
  "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

GET/v1/virtual-accounts/{reference}
Auth
Secret key or dashboard session
Scope
virtual_accounts:read
curl
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

FieldTypeDescription
collectionone-offBound to one amount and one collection. Closed as soon as it is paid.
fundingpermanentYour own wallet deposit account. Stays open and reusable; deposits credit your balance and emit wallet.funding.success.
payment_linkreusableMinted 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.success webhook. 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.failed with reason: "expired". Handle it, or orders sit pending forever.
  • Test accounts never receive real money. Trigger settlement yourself — see Testing.