> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payaza.africa/llms.txt
> Use this file to discover all available pages before exploring further.

# Virtual Accounts

> This guide explains how to collect NGN payments from customers using Payaza virtual accounts - both Dynamic (temporary) and Reserved (permanent).

<Info>
  Virtual Accounts are currently available for **Nigeria (NGN)** only.
</Info>

## Key Requirements

<Info>
  Retrieve your API keys from your dashboard by following the steps on our
  [Authentication](/guides/authentication) page. Use your **public API key** for
  all virtual account requests.
</Info>

Set the following headers on every request:

```json theme={null}
{
  "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](/guides/webhooks) 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)                      |
| --------------------- | --------------------------------------- | ----------------------------------------- |
| **Lifetime**          | 15 – 480 minutes (default 30 min)       | Permanent — never expires                 |
| **Purpose**           | One-off checkout payments               | Per-customer wallets, recurring top-ups   |
| **Amount validation** | Configurable (exact, under, over)       | Not applicable                            |
| **BVN required**      | No                                      | Yes — must be validated first by the user |
| **Best for**          | E-commerce checkouts, one-time invoices | Wallets, savings apps, recurring billing  |

<Warning>
  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.
</Warning>

***

## Supported Virtual Account Providers

Three banks are currently available as virtual account providers. Not all features are supported by all providers.

| Bank                                | Bank Code | Amount Validation | Expiry Control     |
| ----------------------------------- | --------- | ----------------- | ------------------ |
| 78 Finance Company Limited/ Bank 78 | `1067`    | ✓ Yes             | ✓ Yes (15–480 min) |
| Fidelity Bank Limited               | `117`     | ✗ No              | ✗ No               |
| Globus Bank Limited                 | `140`     | ✓ Yes             | ✗ No               |

<Note>
  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.
</Note>

***

## How Virtual Account Collections Work.

<AccordionGroup>
  <Accordion title="Dynamic Virtual Account flow">
    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.

    <Warning>
      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`.
    </Warning>
  </Accordion>

  <Accordion title="Reserved Virtual Account flow">
    <ol className="list-decimal pl-5 space-y-3 mt-2">
      <li>
        Validate the customer's BVN (required for compliance before creating a
        reserved account).
      </li>

      <li>
        Your server calls <strong>Create Virtual Account</strong> with{" "}
        <code>account\_type: "Reserved"</code>, BVN, and customer details.
      </li>

      <li>
        The API returns a permanent <code>account\_number</code> and{" "}
        <code>bank\_name</code>. Store these against the customer record — this
        account does not change.
      </li>

      <li>
        Display the account number to the customer anywhere they need to top up or
        make a payment.
      </li>

      <li>
        When a payment arrives, Payaza fires a webhook. Use the{" "}
        <strong>Get Virtual Account Status</strong> (by account number) to check
        the account's current state.
      </li>
    </ol>
  </Accordion>
</AccordionGroup>

***

### 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

<CodeGroup>
  ```bash cURL theme={null}
  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"
  }
  '
  ```
</CodeGroup>

**Sample response**

```json theme={null}
{
  "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
}
```

<Warning>
  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.
</Warning>

***

### Reserved Virtual Account

<Note>
  The customer's BVN must be validated before creating a Reserved virtual
  account. This is a compliance requirement.
</Note>

<CodeGroup>
  ```bash cURL theme={null}
  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"
  }'
  ```
</CodeGroup>

**Sample response**

```json theme={null}
{
  "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](/api-reference/virtual-accounts/create-reserveddynamic-virtual-account)

***

## Step 2 — Testing: Fund a Test Virtual Account

<Warning>
  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.
</Warning>

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 `""`.

<CodeGroup>
  ```bash cURL — Dynamic VA theme={null}
  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"
    }'
  ```

  ```bash cURL — Reserved VA theme={null}
  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(Jane Smith Wallet)",
      "account_number": "4030623702",
      "initiation_transaction_reference": "",
      "transaction_amount": "10000",
      "currency": "NGN",
      "source_account_number": "9876543210",
      "source_account_name": "Jane Smith",
      "source_bank_name": "Test Bank"
    }'
  ```
</CodeGroup>

**Sample response**

```json theme={null}
{
  "message": "Payment Notification successful",
  "success": true
}
```

<Note>
  A successful fund call will trigger a webhook notification to your configured
  webhook URL, exactly as a real payment would in production.
</Note>

**API reference:** [Fund Test Virtual Account](/api-reference/virtual-accounts/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.

<CodeGroup>
  ```bash cURL theme={null}
  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>' \
  ```
</CodeGroup>

**Sample response Before Payment**

```json theme={null}
{
  "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**

```json theme={null}
{
  "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
}
```

<Note>
  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.
</Note>

**API reference:** [Transaction Status Query](/api-reference/virtual-accounts/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.

<CodeGroup>
  ```bash cURL theme={null}
  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>'
  ```
</CodeGroup>

**Sample response**

```json theme={null}
{
  "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
}
```

<Note>
  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.
</Note>

**API reference:** [Get Virtual Account Status](/api-reference/virtual-accounts/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.

| Event              | When it fires                                 | Account type |
| ------------------ | --------------------------------------------- | ------------ |
| Payment Successful | A customer payment was received and confirmed | Both         |

**Sample `Payment Successful` payload**

```json theme={null}
{
  "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"
}
```

<Note>
  Always verify the webhook signature before processing the payload. See the
  [Webhooks guide](/guides/webhooks) for verification steps.
</Note>

***

***

## 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.
