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

# Virtual Accounts

> Open Nigerian bank accounts programmatically for your customers

## What is a Virtual Account?

A virtual account is a **real Nigerian bank account number**. It can receive transfers from any bank in Nigeria, but it lives inside your master Union Bank account.

Think of it like suites in a hotel: each suite has its own room number (the account number your customer sees), but they all share the same hotel address (your master Union Bank account). When a guest pays the hotel, the front desk knows exactly which suite the payment is for.

That front desk is your webhook. When money arrives in any virtual account, we notify your server immediately with the full details of who paid and how much.

<Info>
  Virtual accounts are denominated in Nigerian Naira (NGN) only. They carry Union Bank's sort code: **032**.
</Info>

***

## Two Types of Virtual Accounts

<CardGroup cols={2}>
  <Card title="Static Account" icon="user">
    **Permanent.** The account number never expires and is always assigned to the same customer or purpose.

    **Best for:** Wallet top-ups, recurring billing, dedicated customer accounts. When your customer wants to fund their wallet, they always transfer to the same number. No need to generate a new one each time.
  </Card>

  <Card title="Dynamic Account" icon="clock">
    **Temporary.** The account number expires after a time you set (e.g. 2 hours, 24 hours).

    **Best for:** One-time payment links, invoice payments, event ticket orders. Generate the account, share it with your customer, and it disappears once expired. Clean and secure.
  </Card>
</CardGroup>

***

## Create a Virtual Account

Before you start, you need:

* Your `partnerId` (visible in the sandbox dashboard)
* An API key (you'll get this when your sandbox account is provisioned)
* An idempotency key: a random unique string you generate for each request (see note below)

<Note>
  **What is an idempotency key?** It's a unique ID you attach to every create request. If your network drops after you send the request but before you receive a response, you can safely resend the same request with the same idempotency key. We'll return the original result instead of creating a duplicate account. Use a UUID. Most languages and operating systems have a built-in generator.
</Note>

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST https://api-partner.onecluster.co/api/v1/accounts \
      -H "Authorization: ApiKey ubn_sb_your_key_here" \
      -H "Content-Type: application/json" \
      -H "X-Idempotency-Key: $(uuidgen)" \
      -d '{
        "partnerId": "your-partner-id",
        "accountType": "STATIC"
      }'
    ```
  </Tab>

  <Tab title="JavaScript (fetch)">
    ```javascript theme={null}
    const { randomUUID } = require('crypto');

    const response = await fetch('https://api-partner.onecluster.co/api/v1/accounts', {
      method: 'POST',
      headers: {
        'Authorization': 'ApiKey ubn_sb_your_key_here',
        'Content-Type': 'application/json',
        'X-Idempotency-Key': randomUUID(),
      },
      body: JSON.stringify({
        partnerId: 'your-partner-id',
        accountType: 'STATIC',
      }),
    });

    const account = await response.json();
    console.log(account.accountNumber); // e.g. "9023456789"
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import uuid
    import requests

    response = requests.post(
        'https://api-partner.onecluster.co/api/v1/accounts',
        headers={
            'Authorization': 'ApiKey ubn_sb_your_key_here',
            'Content-Type': 'application/json',
            'X-Idempotency-Key': str(uuid.uuid4()),
        },
        json={
            'partnerId': 'your-partner-id',
            'accountType': 'STATIC',
        }
    )

    account = response.json()
    print(account['accountNumber'])  # e.g. "9023456789"
    ```
  </Tab>
</Tabs>

**Successful response:**

```json theme={null}
{
  "status": "success",
  "data": {
    "accountNumber": "9023456789",
    "accountName": "UBN / YOUR BUSINESS NAME",
    "bankCode": "032",
    "bankName": "Union Bank of Nigeria",
    "accountType": "STATIC",
    "currency": "NGN",
    "partnerId": "your-partner-id",
    "createdAt": "2026-03-25T10:00:00+01:00"
  }
}
```

### Create a Dynamic (Expiring) Account

To create a temporary account, set `accountType` to `DYNAMIC` and pass an `expiresAt` timestamp in ISO 8601 format:

```bash theme={null}
curl -X POST https://api-partner.onecluster.co/api/v1/accounts \
  -H "Authorization: ApiKey ubn_sb_your_key_here" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -d '{
    "partnerId": "your-partner-id",
    "accountType": "DYNAMIC",
    "expiresAt": "2026-03-25T12:00:00+01:00",
    "reference": "ORDER-00123"
  }'
```

<Tip>
  Pass your own `reference` (e.g. an order ID) when creating a dynamic account. It is returned in the webhook payload when money arrives, so you can match the payment to the right order without any extra lookup.
</Tip>

***

## Check Account Balance

```bash theme={null}
curl -X GET https://api-partner.onecluster.co/api/v1/accounts/9023456789/balance \
  -H "Authorization: ApiKey ubn_sb_your_key_here"
```

**Response:**

```json theme={null}
{
  "status": "success",
  "data": {
    "accountNumber": "9023456789",
    "availableBalance": 150000.00,
    "ledgerBalance": 150000.00,
    "currency": "NGN"
  }
}
```

`availableBalance` is what your customer can actually spend. `ledgerBalance` may include funds that are still processing (e.g. a credit still pending confirmation).

***

## Get Account Statement

Pull a list of transactions for a virtual account within a date range. Dates follow the `YYYY-MM-DD` format.

```bash theme={null}
curl -X GET "https://api-partner.onecluster.co/api/v1/accounts/9023456789/statement?from=2026-03-01&to=2026-03-25&page=1&limit=20" \
  -H "Authorization: ApiKey ubn_sb_your_key_here"
```

**Response:**

```json theme={null}
{
  "status": "success",
  "data": {
    "accountNumber": "9023456789",
    "from": "2026-03-01",
    "to": "2026-03-25",
    "transactions": [
      {
        "transactionRef": "COL-20260325-001",
        "type": "CREDIT",
        "amount": 50000.00,
        "currency": "NGN",
        "narration": "Transfer from ADEBAYO JOHNSON",
        "balanceAfter": 150000.00,
        "timestamp": "2026-03-25T10:00:00+01:00"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 1
    }
  }
}
```

***

## Receiving Money

When anyone does a bank transfer to a virtual account number from any Nigerian bank (including mobile apps like GTBank, Access, or First Bank), the money lands in your master Union Bank account. You don't need to do anything to "pull" it in.

To be notified the moment a payment arrives, configure a webhook. We'll send an HTTP POST to your server with full details of the transaction, including the sender's name and account number.

See the [Webhooks guide](/products/webhooks) to set this up.

<Warning>
  Don't poll the balance to detect incoming payments. Webhooks are faster, more reliable, and don't count against your rate limit quota. Use webhooks for anything customer-facing.
</Warning>

***

## Limits and Constraints

| Constraint                              | Value                                |
| --------------------------------------- | ------------------------------------ |
| Currency                                | NGN only                             |
| Maximum static accounts per partner     | 10,000 (contact support to increase) |
| Maximum dynamic accounts active at once | 5,000                                |
| Minimum dynamic account expiry          | 5 minutes from creation              |
| Maximum dynamic account expiry          | 72 hours from creation               |
| Account number format                   | 10-digit NUBAN                       |

<Note>
  If you need higher limits for your use case (e.g. you are building a large-scale savings or lending product), contact [baas-support@unionbank.ng](mailto:baas-support@unionbank.ng) with your expected monthly volume.
</Note>

***

## Frequently Asked Questions

<AccordionGroup>
  <Accordion title="Do virtual accounts have a real sort code?">
    Yes. All virtual accounts issued through this platform carry Union Bank's sort code: **032**. When your customer's bank displays the destination bank during a transfer, they will see "Union Bank of Nigeria."
  </Accordion>

  <Accordion title="Can my customers see 'Union Bank' when they transfer?">
    Yes. Your customers will see Union Bank of Nigeria as the receiving bank in their banking app or USSD confirmation screen. The account name will appear as set during account creation (typically your business name or a customer-specific label).
  </Accordion>

  <Accordion title="What happens when a dynamic account expires?">
    Any transfer sent to an expired dynamic account number is **rejected at the network level** and returned to the sender's bank. No money reaches your master account. The reversal typically takes 1 to 3 business days, depending on the sending bank.
  </Accordion>

  <Accordion title="Can I assign the same virtual account to multiple customers?">
    No. Assign each virtual account to exactly one customer or one purpose. If two customers share an account number, you can't tell who made a given payment. The whole point of virtual accounts is pre-sorted, attributable payments.
  </Accordion>

  <Accordion title="Can I close or deactivate a virtual account?">
    Yes. Send a `DELETE` request to `/api/v1/accounts/{accountNumber}`. Static accounts can be deactivated at any time. Once deactivated, any inbound transfers are rejected and returned to the sender.
  </Accordion>
</AccordionGroup>
