Skip to main content
Virtual Accounts are currently available for Nigeria (NGN) only.

Key Requirements

Retrieve your API keys from your dashboard by following the steps on our Authentication page. Use your public API key for all virtual account requests.
Set the following headers on every request:
{
  "Authorization": "Payaza <Your public API key encoded in base 64>"
}
Make sure your webhook URL is saved on the dashboard and configured to accept POST requests. See our Webhooks guide for setup instructions.

Dynamic vs Reserved — which type do you need?

Virtual accounts come in two types. Choose based on your use case before integrating.
Dynamic (Temporary)Reserved (Permanent)
Lifetime15 – 480 minutes (default 30 min)Permanent — never expires
PurposeOne-off checkout paymentsPer-customer wallets, recurring top-ups
Amount validationConfigurable (exact, under, over)Not applicable
BVN requiredNoYes — must be validated first by the user
Best forE-commerce checkouts, one-time invoicesWallets, savings apps, recurring billing
Always ensure you are using the correct account type for your use case. Using a Dynamic account where a Reserved account is needed (or vice versa) will cause integration issues that are difficult to debug.

Supported Virtual Account Providers

Three banks are currently available as virtual account providers. Not all features are supported by all providers.
BankBank CodeAmount ValidationExpiry Control
78 Finance Company Limited/ Bank 781067✓ Yes✓ Yes (15–480 min)
Fidelity Bank Limited117✗ No✗ No
Globus Bank Limited140✓ Yes✗ No
Amount validation (has_amount_validation) is only honoured by 78 Finance and Globus Bank respectively. Passing these fields with Fidelity Bank will have no effect.

How Virtual Account Collections Work.

  1. Your server calls Create Virtual Account with account_type: "Dynamic", the customer details, amount, and expiry time.
  2. The API returns an account_number and bank_name. Display these to your customer along with the exact amount to pay.
  3. The customer makes a bank transfer to the virtual account from their own bank app or USSD.
  4. Payaza receives the transfer and fires a webhook to your configured URL.
  5. Your server calls Transaction Status Query (using account_reference) to confirm the payment, or relies on the webhook.
Dynamic accounts expire. If the customer does not pay before the account expires, you must create a new virtual account and show the updated details. Never reuse an expired account_reference.
  1. Validate the customer’s BVN (required for compliance before creating a reserved account).
  2. Your server calls Create Virtual Account with account_type: “Reserved”, BVN, and customer details.
  3. The API returns a permanent account_number and bank_name. Store these against the customer record — this account does not change.
  4. Display the account number to the customer anywhere they need to top up or make a payment.
  5. When a payment arrives, Payaza fires a webhook. Use the Get Virtual Account Status (by account number) to check the account’s current state.

Step 1

Create a Virtual Account

A single endpoint handles both Dynamic and Reserved account creation. The account_type field controls which type is created.

Dynamic Virtual Account

curl --request POST \
  --url https://api.payaza.africa/live/merchant-collection/merchant/virtual_account/generate_virtual_account \
  --header 'Authorization: Payaza <Your public API key encoded in base 64>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "account_name": "Test DVA",
  "account_type": "Dynamic",
  "bank_code": "140",
  "bvn": "",
  "has_amount_validation": "true",
  "account_reference": "Ref123456780",
  "customer_first_name": "John",
  "customer_last_name": "Doe",
  "customer_email": "johndoe@gmail.com",
  "customer_phone_number": "07012345678",
  "transaction_description": "Test Description",
  "transaction_amount": "1000",
  "expires_in_minutes": "20"
}
'
Sample response
{
  "message": "Virtual Account generated successfully",
  "data": {
    "account_name": "Payaza(Test DVA)",
    "account_number": "3340009013",
    "account_type": "Dynamic",
    "bank_name": "GLOBUS BANK",
    "account_reference": "accRef123",
    "account_expired": false,
    "message": "Virtual Account generated successfully",
    "transaction_id": 49555930,
    "transaction_amount_payable": 100,
    "transaction_reference": "accRef123",
    "expires_in_minutes": 20
  },
  "success": true
}
Always show the customer the exact transaction_amount_payable from the response. If has_amount_validation is true, payments of a different amount will be rejected by the bank.

Reserved Virtual Account

The customer’s BVN must be validated before creating a Reserved virtual account. This is a compliance requirement.
curl --request POST \
  --url https://api.payaza.africa/live/merchant-collection/merchant/virtual_account/generate_virtual_account \
  --header 'Authorization: Payaza <Your Public API key encoded in base 64>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "account_name": "Test Reserved VA",
  "account_type": "Static",
  "bank_code": "140",
  "bvn": "3232123454",
  "bvn_validated": true,
  "account_reference": "accRef123",
  "customer_first_name": "John",
  "customer_last_name": "Doe",
  "customer_email": "johndoe@gmail.com",
  "customer_phone_number": "07012345678"
}'
Sample response
{
  "message": "Virtual Account generated successfully",
  "data": {
    "account_name": "Payaza(Test Reserved VA)",
    "account_number": "3330772330",
    "account_type": "Static",
    "bank_name": "GLOBUS BANK",
    "account_reference": "accRef123",
    "account_expired": false,
    "message": "Virtual Account generated successfully"
  },
  "success": true
}
API reference: Create Reserved/Dynamic Virtual Account

Step 2 — Testing: Fund a Test Virtual Account

This endpoint is available in the test environment only. Do not call it in production. In the live environment, customers fund virtual accounts by making real bank transfers.
Use this endpoint to simulate a customer payment during development and test your webhook and status-check logic before going live. For Dynamic accounts, pass the account_reference used at creation as initiation_transaction_reference. For Reserved accounts, leave initiation_transaction_reference as an empty string "".
curl --request POST \
  --url https://api.payaza.africa/live/merchant-collection/payaza/virtual_account/fund_test_virtual_account \
  --header 'Authorization: Payaza <Your Public API key encoded in base 64>' \
  --header 'Content-Type: application/json' \
  --data '{
    "account_name": "Payaza(Acme Checkout)",
    "account_number": "3340009013",
    "initiation_transaction_reference": "TXN-REF-20240501-001",
    "transaction_amount": "5000",
    "currency": "NGN",
    "source_account_number": "0123456789",
    "source_account_name": "John Doe",
    "source_bank_name": "Test Bank"
  }'
Sample response
{
  "message": "Payment Notification successful",
  "success": true
}
A successful fund call will trigger a webhook notification to your configured webhook URL, exactly as a real payment would in production.
API reference: Fund Test Virtual Account

Step 3a — Transaction Status Query (Dynamic VAs only)

Use this endpoint to check the payment status of a Dynamic virtual account transaction. Query by the account_reference value used when creating the virtual account.
curl --request GET \
  --url 'https://api.payaza.africa/live/merchant-collection/transfer_notification_controller/transaction-query?transaction_reference={transactionReference}' \
  --header 'Authorization: Payaza <Your Public API key encoded in base 64>' \
Sample response Before Payment
{
  "message": "Transaction data found",
  "data": {
    "transaction_reference": "Ref1234567890",
    "amount_received": 1000,
    "transaction_fee": 0,
    "transaction_status": "Initialized",
    "sender_name": null,
    "sender_account_number": null,
    "source_bank_name": null,
    "initiated_date": "2024-10-10 18:18:09.117098",
    "current_status_date": null,
    "currency": "NGN",
    "session_id": "",
    "merchant_transaction_reference": "Ref1234567890",
    "transaction_type": "VirtualAccount",
    "virtual_account_number": "7000009348",
    "status_reason": "Awaiting customer to complete payment"
  },
  "success": true
}
Sample response After Payment
{
  "message": "Transaction data found",
  "data": {
    "transaction_reference": "Ref1234567890",
    "amount_received": 1000,
    "transaction_fee": 0,
    "transaction_status": "Completed",
    "sender_name": "Jill Stones",
    "sender_account_number": "0123456789",
    "source_bank_name": null,
    "initiated_date": "2024-10-10 19:41:04.312997",
    "current_status_date": "2024-10-10 19:41:04.312977",
    "currency": "NGN",
    "session_id": "8c35b4b0-161f-4f94-900b-05c358461d13",
    "merchant_transaction_reference": "Ref1234567890",
    "transaction_type": "VirtualAccount",
    "virtual_account_number": "7000009348",
    "status_reason": "Transfer Successful"
  },
  "success": true
}
There are only two possible transaction_status values: "Initialized" (awaiting payment) and "Completed" (payment received). There is no “Failed” status — an unpaid Dynamic account simply expires.
API reference: Transaction Status Query

Step 3b — Get Virtual Account Status (Reserved VAs only)

Use this endpoint to retrieve the current status and summary of a Reserved virtual account by its account number.
curl --request GET \
  --url https://api.payaza.africa/live/merchant-collection/merchant/virtual_account/detail/virtual_account/4030623702 \
  --header 'Authorization: Payaza <Your Public API key encoded in base 64>'
Sample response
{
  "message": "Successful",
  "data": {
    "virtual_account_number": "4030623702",
    "virtual_account_name": "Payaza(Test Reserved VA)",
    "account_type": "static",
    "account_status": "Active",
    "total_transactions": 0,
    "provider_bank_name": "78 FINANCE COMPANY LIMITED",
    "provider_bank_code": "110072"
  },
  "success": true
}
This endpoint returns the account’s status and lifetime transaction count — not individual transaction details. For per-payment details on a Reserved account, rely on webhook events.
API reference: Get Virtual Account Status

Webhooks

Payaza fires webhook events for all successful virtual account payments. Webhooks are the recommended way to be notified of incoming payments in real time.
EventWhen it firesAccount type
Payment SuccessfulA customer payment was received and confirmedBoth
Sample Payment Successful payload
{
  "transaction_reference": "testpayment",
  "transaction_status": "Funds Received",
  "virtual_account_number": "7540004220",
  "virtual_account_type": "Dynamic",
  "transaction_fee": "100.00",
  "amount_received": "100.00",
  "initiated_date": "2026-01-21 10:12:21",
  "current_status_date": "2026-01-21 10:12:21",
  "received_from": {
    "account_name": "JOHN DOE",
    "account_number": "8181234567",
    "bank_name": "OPAY"
  },
  "merchant_reference": "testpayment",
  "status": "Completed",
  "session_id": "100345263561490223460447749045",
  "channel": "VirtualAccount",
  "branch": false,
  "currency_code": "NGN",
  "payaza_account_reference": "",
  "narration": "",
  "business_fk": 100,
  "customer": {
    "email_address": "johndoe@gmail.com",
    "first_name": "John",
    "last_name": "Doe",
    "mobile_number": "07012345678"
  },
  "request_amount": 100.0,
  "amount_validation": "EXACT"
}
Always verify the webhook signature before processing the payload. See the Webhooks guide for verification steps.


Developer notes

  • Always generate a unique account_reference per virtual account creation to avoid conflicts when querying transaction status.
  • For Dynamic accounts, show the customer a countdown timer so they know when the account expires. Minimum expiry is 15 minutes; maximum is 480 minutes.(78 Finance Company Limited/ Bank 78)
  • For Reserved accounts, store the account_number and bank_name permanently in your database against the customer record — you should never need to re-create a Reserved account for the same customer.
  • The account_name displayed to the customer will always be prefixed with "Payaza(" by the bank (e.g. "Payaza(Acme Checkout)"). This is expected behaviour.
  • Webhooks are the recommended primary mechanism for payment confirmation. Use Transaction Status Query as a fallback only when a webhook is not received within your expected timeout window.
  • Transactions have only two terminal states: Completed (paid) and Initialized . There is no “Failed” state.
  • The Fund Test Virtual Account endpoint must only be called with test API Key. It will not work in the live environment and is not a real payment.