# Virtual Accounts

Assign dedicated bank account numbers to your customers so they can pay you via bank transfer — at any time, from any bank, without a checkout page.

***

Most payment collection methods require your customer to be online at the exact moment you want to collect. They click a button, land on a checkout page, enter their card details, and pay. That works beautifully for e-commerce checkouts and subscription sign-ups, but it falls apart when the customer's preferred payment method is a plain bank transfer.

Virtual accounts flip the model. Instead of pulling money from a customer through a checkout flow, you give them a dedicated bank account number — one that belongs to them and only them — and they push money to you whenever they're ready. The transfer is automatically detected, matched to the right customer, and recorded as a payment in your Tendar account. No redirects, no card details, no checkout abandonment.

This is especially powerful for lending platforms, rent collection, invoice payments, and any scenario where customers are accustomed to paying via bank transfer. In Nigeria, where bank transfers are second nature, virtual accounts are often the most reliable collection channel you can offer.

## Before you begin

To use virtual accounts, you need:

1. **An active Tendar account** with a valid API key. All requests require a Bearer token:

   ```bash
   Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx
   ```
2. **An active subscription** on your Tendar company account. Every virtual account endpoint checks your subscription status before processing.
3. **Customer information** — virtual accounts are tied to real people. You'll need the customer's full name, BVN, phone number, date of birth, gender, address, and email. If the customer already exists in your Tendar user directory, you can pass their `user_id` and Tendar will pull their details automatically.

{% hint style="info" %}
Virtual accounts are provisioned through **Focus MFB**, a licensed microfinance bank. The account numbers are real bank accounts — customers can transfer to them from any Nigerian bank using NIP (Nigeria Inter-Bank Payment System). This is not a simulation; money actually moves through the banking system.
{% endhint %}

## How it works

The virtual account lifecycle has three phases: **create**, **receive payments**, and **manage**.

```
┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   Your server    │     │   Tendar         │     │   Focus MFB      │
│   requests a     │────▶│   validates the  │────▶│   opens a real   │
│   virtual        │     │   customer data  │     │   bank account   │
│   account        │     │   & provisions   │     │   and returns    │
│                  │     │   the account    │     │   an account     │
│                  │     │                  │     │   number         │
└──────────────────┘     └──────────────────┘     └──────────────────┘

                            ···  Time passes  ···

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   Customer       │     │   Their bank     │     │   Focus MFB      │
│   initiates a    │────▶│   sends the      │────▶│   receives the   │
│   bank transfer  │     │   transfer via   │     │   funds &        │
│   to the virtual │     │   NIP            │     │   notifies       │
│   account number │     │                  │     │   Tendar         │
└──────────────────┘     └──────────────────┘     └────────┬─────────┘
                                                           │
                         ┌──────────────────┐     ┌────────▼─────────┐
                         │   Tendar         │     │   Tendar         │
                         │   sends webhook  │◀────│   verifies the   │
                         │   to your server │     │   payment,       │
                         │                  │     │   records it &   │
                         │                  │     │   credits your   │
                         │                  │     │   wallet         │
                         └──────────────────┘     └──────────────────┘
```

The beauty of this flow is that **steps 2 and 3 happen without any API calls from you**. Once the virtual account exists, the customer can transfer money to it at any time, and Tendar handles everything — detection, verification, recording, and notification.

Let's walk through each phase.

***

## Phase 1: Create a virtual account

To provision a virtual account, send a `POST` request with the customer's KYC (Know Your Customer) details.

**Endpoint**

```http
POST /api/v1/virtual-account/create
```

**Request body**

```json
{
    "user_id": "",
    "first_name": "John",
    "last_name": "Doe",
    "phone_number": "+2348181765499",
    "gender": "male",
    "place_of_birth": "Lagos",
    "date_of_birth": "15/03/1995",
    "email": "john@example.com",
    "bvn": "22549535647",
    "address": "26, Mabinuori Street, Ikosi, Lagos",
    "metadata": {
        "customer_tier": "premium",
        "loan_id": "loan-4829"
    }
}
```

### What each field means

| Field            | Type   | Required | Description                                                                                                                                                                                                                                                                                           |
| ---------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_id`        | string | No       | The Tendar user ID of the customer (e.g., `usr-hnlfkizfoc`). If provided, Tendar looks up the user and automatically fills in `first_name`, `last_name`, `email`, `phone_number`, `gender`, `date_of_birth`, and `address` from their profile. You can still pass individual fields to override them. |
| `first_name`     | string | Yes      | The customer's first name.                                                                                                                                                                                                                                                                            |
| `last_name`      | string | Yes      | The customer's last name.                                                                                                                                                                                                                                                                             |
| `phone_number`   | string | Yes      | The customer's phone number in E.164 format (e.g., `+2348181765499`).                                                                                                                                                                                                                                 |
| `gender`         | string | Yes      | The customer's gender. Must be `male` or `female`.                                                                                                                                                                                                                                                    |
| `place_of_birth` | string | Yes      | The customer's place of birth (city or state).                                                                                                                                                                                                                                                        |
| `date_of_birth`  | string | Yes      | The customer's date of birth in `DD/MM/YYYY` format (e.g., `15/03/1995`).                                                                                                                                                                                                                             |
| `email`          | string | Yes      | The customer's email address.                                                                                                                                                                                                                                                                         |
| `bvn`            | string | Yes      | The customer's Bank Verification Number. Must be exactly 11 digits. This is validated against the banking system.                                                                                                                                                                                     |
| `address`        | string | Yes      | The customer's residential address.                                                                                                                                                                                                                                                                   |
| `metadata`       | object | No       | Any custom key-value pairs you want to attach to the virtual account. Stored alongside the account and returned in all API responses and webhook payloads.                                                                                                                                            |

{% hint style="warning" %}
The `account_reference_number` field exists on the virtual account object but is **not allowed** when creating an account through the API. If you pass it, the request will be rejected. Account reference numbers are managed internally.
{% endhint %}

### Using `user_id` to simplify creation

If the customer is already registered in your Tendar user directory, you can skip most of the fields and just pass their `user_id`:

```json
{
    "user_id": "usr-hnlfkizfoc",
    "bvn": "22549535647",
    "place_of_birth": "Lagos"
}
```

Tendar will look up the user, pull their first name, last name, email, phone number, gender, date of birth, and address from their profile, and use those to create the virtual account. You only need to provide fields that aren't on the user's profile — like `bvn` and `place_of_birth`, which aren't part of the standard user object.

If the `user_id` doesn't match any user in your company's directory, you'll get a `404 user not found` error.

### What happens behind the scenes

When you send the create request, Tendar:

1. **Checks charges** — virtual account creation is a billable action. Tendar verifies that your company can be charged for the `virtual_account.create` pricing item before proceeding.
2. **Resolves the customer** — if you provided a `user_id`, Tendar fetches the user's profile and merges their details into the request.
3. **Validates the input** — ensures all required fields are present, the phone number is in E.164 format, the gender is valid, the date of birth is in the correct format, and the BVN is exactly 11 digits.
4. **Provisions the account with Focus MFB** — sends the customer's details to Focus MFB's API, which opens a real Tier 1 savings/current account in the customer's name. Focus MFB returns the new account number.
5. **Stores the virtual account** — saves the account in Tendar's database, linked to your company and (optionally) to the user.
6. **Finalizes the charge** — the `virtual_account.create` usage is recorded against your subscription.

### Response

**`200 OK`**

```json
{
    "data": {
        "created_at": "2025-12-01T02:59:34.269Z",
        "updated_at": "2025-12-01T02:59:34.269Z",
        "id": "692d04961e4102c1fda719e3",
        "account_number": "1100017689",
        "first_name": "John",
        "last_name": "Doe",
        "phone_number": "+2348181765499",
        "gender": "male",
        "place_of_birth": "Lagos",
        "date_of_birth": "15/03/1995",
        "email": "john@example.com",
        "address": "26, Mabinuori Street, Ikosi, Lagos",
        "account_tier": "1",
        "status": "active",
        "metadata": {
            "customer_tier": "premium",
            "loan_id": "loan-4829"
        },
        "service_provider": "focus_mfb",
        "freeze_status": "inactive"
    },
    "error": false,
    "message": "Virtual account created successfully"
}
```

The key field in the response is **`account_number`** — this is the real bank account number your customer will transfer money to. Give it to them however makes sense for your product: display it on a payment page, send it in an SMS, include it on an invoice, or surface it in your app's "How to Pay" section.

Notice a few things about the initial state:

* **`status` is `active`** — the account is ready to receive transfers immediately.
* **`freeze_status` is `inactive`** — the account is not frozen. We'll cover freezing later.
* **`account_tier` is `1`** — this is a Tier 1 account, which has daily and cumulative balance limits set by the Central Bank of Nigeria (CBN). For most collection use cases, Tier 1 is sufficient.
* **`service_provider` is `focus_mfb`** — the account is held at Focus MFB. Currently, this is the only supported provider for virtual accounts.
* **`bvn` is not in the response** — for security, the BVN is stripped from API responses. It's stored securely but never returned.

***

## Phase 2: Receive payments

Once the virtual account is created, the customer can transfer money to it from any Nigerian bank. There's no API call required on your end — the process is entirely customer-initiated.

### What happens when a customer transfers money

{% stepper %}
{% step %}

### The customer initiates a transfer

They open their banking app (or walk into a bank branch), enter the virtual account number as the recipient, specify an amount, and send the transfer.
{% endstep %}

{% step %}

### The transfer arrives at Focus MFB

The Nigerian Inter-Bank Payment System (NIP) routes the transfer to the Focus MFB account.
{% endstep %}

{% step %}

### Focus MFB notifies Tendar

A webhook notification is sent to Tendar with the transfer details: amount, sender account, reference, and the recipient account number.
{% endstep %}

{% step %}

### Tendar processes the notification

The `virtual_account.payment` event is queued for processing. Tendar:

* Looks up the virtual account by account number to identify which company and customer it belongs to.
* Verifies the transaction status with Focus MFB to confirm the transfer actually succeeded.
* Calculates the applicable fees based on your company's pricing configuration (or the default 2% capped at ₦5,000).
* Creates a payment record tied to the virtual account, the customer, and your company.
* Credits the payment amount to your **Tendar wallet**.
* Records the charge against your subscription.
  {% endstep %}

{% step %}

### Tendar sends a webhook to you

A `payment.success` event is delivered to your company's webhook URL with the full payment details.
{% endstep %}
{% endstepper %}

### The payment that gets created

When a transfer lands in a virtual account, Tendar automatically creates a payment record. This payment looks just like any other payment in the system, but with a few virtual-account-specific details:

* **`payment_channel`** is `"virtual_account"` — so you can distinguish VA payments from card or bank transfer payments.
* **`provider`** is `"focus_mfb"` — indicating the transfer came through Focus MFB.
* **`virtual_account`** contains the ID of the virtual account that received the transfer.
* **`account_number`** is the virtual account number.
* **`sender_account_number`**, **`sender_account_name`**, and **`sender_bank`** contain the sender's banking details (when available).

### Webhook payload

When a virtual account receives a transfer, your webhook endpoint receives an event like this:

```json
{
    "company_id": "64245949dbcc9f021a769e65",
    "event": "payment.success",
    "service": "recollection",
    "data": {
        "id": "692d1f3a1e4102c1fda719f7",
        "user_id": "usr-hnlfkizfoc",
        "email": "john@example.com",
        "payment_channel": "virtual_account",
        "provider": "focus_mfb",
        "amount": 50000,
        "fees": 1000,
        "currency": "NGN",
        "reference": "FT2025120100001234",
        "status": "success",
        "account_number": "1100017689",
        "sender_account_number": "0123456789",
        "sender_account_name": "JANE SMITH",
        "sender_bank": "GTBank",
        "paid_at": "2025-12-01T14:22:31Z",
        "metadata": {
            "customer_tier": "premium",
            "loan_id": "loan-4829"
        }
    }
}
```

When you receive this event, you can safely credit the customer's balance, mark an invoice as paid, record a loan repayment, or trigger whatever business logic you need.

### Fees

Virtual account payments are charged based on your company's pricing configuration. The default pricing is:

| Parameter     | Value                                     |
| ------------- | ----------------------------------------- |
| **Cost**      | 2% of the transfer amount                 |
| **Cost type** | Percentage                                |
| **Capped at** | ₦5,000                                    |
| **Billing**   | Postpaid (charged per subscription cycle) |

For example:

* A ₦10,000 transfer incurs a ₦200 fee (2% × ₦10,000).
* A ₦500,000 transfer incurs a ₦5,000 fee (2% × ₦500,000 = ₦10,000, but capped at ₦5,000).

If your company has a custom pricing plan configured for the `virtual_account.payment` item, those rates apply instead of the defaults.

The fee is recorded on the payment's `fees` field and is also charged against your subscription as a postpaid item.

***

## Phase 3: Manage virtual accounts

After creating a virtual account, you have several management operations available: checking the balance, fetching account details, listing all accounts, and freezing or unfreezing an account.

### Check the balance

To see how much money is sitting in a virtual account, use the balance enquiry endpoint.

**Endpoint**

```http
GET /api/v1/virtual-account/balance-enquiry/:account_number
```

**Example**

```bash
curl https://api.tendar.io/api/v1/virtual-account/balance-enquiry/1100017689 \
  -H "Authorization: Bearer sk_live_xxxxxxxx"
```

**Response**

```json
{
    "data": {
        "available_balance": 50000,
        "ledger_balance": 50000,
        "withdraw_balance": 50000,
        "account_type": "Savings/Current Account"
    },
    "error": false,
    "message": "Balance enquiry successful"
}
```

#### Understanding the balance fields

| Field               | Description                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| `available_balance` | The amount currently available for transactions. This is the balance minus any holds or pending debits. |
| `ledger_balance`    | The total balance on the account, including pending transactions.                                       |
| `withdraw_balance`  | The amount that can be withdrawn from the account.                                                      |
| `account_type`      | The type of account (typically "Savings/Current Account" for Tier 1 virtual accounts).                  |

The balance is fetched in real-time from Focus MFB, so it always reflects the latest state of the account — including transfers that may have just arrived.

{% hint style="info" %}
The balance in the virtual account is separate from your Tendar wallet balance. When a customer transfers money to the virtual account, the amount is credited to your Tendar wallet as a payment. The virtual account balance reflects the actual bank account balance at Focus MFB, which may differ from what's in your wallet depending on how funds are swept.
{% endhint %}

### Fetch account details

To retrieve the full details of a virtual account — including its current status, balance, and tier — use the fetch endpoint.

**Endpoint**

```http
GET /api/v1/virtual-account/fetch/:account_number
```

**Example**

```bash
curl https://api.tendar.io/api/v1/virtual-account/fetch/1100017689 \
  -H "Authorization: Bearer sk_live_xxxxxxxx"
```

**Response**

```json
{
    "data": {
        "created_at": "2025-12-01T02:59:34.269Z",
        "updated_at": "2025-12-01T02:59:34.269Z",
        "id": "692d04961e4102c1fda719e3",
        "account_number": "1100017689",
        "first_name": "John",
        "last_name": "Doe",
        "phone_number": "+2348181765499",
        "gender": "male",
        "place_of_birth": "Lagos",
        "date_of_birth": "15/03/1995",
        "email": "john@example.com",
        "address": "26, Mabinuori Street, Ikosi, Lagos",
        "account_tier": "1",
        "status": "active",
        "metadata": {
            "customer_tier": "premium",
            "loan_id": "loan-4829"
        },
        "available_balance": 50000,
        "ledger_balance": 50000,
        "service_provider": "focus_mfb",
        "freeze_status": "inactive"
    },
    "error": false,
    "message": "Virtual account details fetched successfully"
}
```

This endpoint merges Tendar's stored data (name, metadata, creation date) with real-time data from Focus MFB (status, balance, tier). The `bvn` is excluded from the response for security reasons.

### List all virtual accounts

To see all virtual accounts created under your company, use the list endpoint.

**Endpoint**

```http
GET /api/v1/virtual-account/list
```

**Query parameters**

| Parameter         | Description                                                             |
| ----------------- | ----------------------------------------------------------------------- |
| `page`            | Page number (default: 1).                                               |
| `limit`           | Results per page (default: 20).                                         |
| `user_id`         | Filter by Tendar user ID.                                               |
| `status`          | Filter by account status (e.g., `active`).                              |
| `freeze_status`   | Filter by freeze status (`active` for frozen, `inactive` for unfrozen). |
| `created_at`      | Filter by creation date.                                                |
| `sort-created_at` | Sort by creation date (`asc` or `desc`).                                |
| `sort-updated_at` | Sort by last update (`asc` or `desc`).                                  |

**Example**

```bash
curl "https://api.tendar.io/api/v1/virtual-account/list?page=1&limit=20&sort-created_at=desc" \
  -H "Authorization: Bearer sk_live_xxxxxxxx"
```

**Response**

```json
{
    "data": {
        "total": 47,
        "page": 1,
        "per_page": 20,
        "prev": 0,
        "next": 2,
        "total_page": 3,
        "docs": [
            {
                "created_at": "2025-12-01T02:59:34.269Z",
                "updated_at": "2025-12-01T02:59:34.269Z",
                "id": "692d04961e4102c1fda719e3",
                "account_number": "1100017689",
                "first_name": "John",
                "last_name": "Doe",
                "phone_number": "+2348181765499",
                "gender": "male",
                "place_of_birth": "Lagos",
                "date_of_birth": "15/03/1995",
                "email": "john@example.com",
                "address": "26, Mabinuori Street, Ikosi, Lagos",
                "account_tier": "1",
                "status": "active",
                "metadata": {
                    "customer_tier": "premium",
                    "loan_id": "loan-4829"
                },
                "service_provider": "focus_mfb",
                "freeze_status": "inactive"
            }
        ]
    },
    "error": false,
    "message": "Virtual accounts listed successfully"
}
```

The list endpoint returns account data as stored in Tendar's database. Unlike the fetch endpoint, it does **not** make a real-time call to Focus MFB for each account, so the `available_balance` and `ledger_balance` fields are not included. Use the fetch or balance-enquiry endpoint if you need live balance data for a specific account.

***

## Freezing and unfreezing accounts

Sometimes you need to temporarily prevent a virtual account from receiving payments — perhaps a customer's account is under review, a loan has been fully repaid and you don't want stray transfers, or you need to investigate suspicious activity.

Tendar supports **freezing** a virtual account (blocking incoming transfers) and **unfreezing** it (allowing transfers again). These operations are available through the gRPC interface used by the Tendar API gateway and internal services.

### How freeze status works

Every virtual account has a `freeze_status` field that can be:

| Freeze status | Meaning                                                                                      |
| ------------- | -------------------------------------------------------------------------------------------- |
| `inactive`    | The account is **not frozen**. It can receive transfers normally. This is the default state. |
| `active`      | The account is **frozen**. Incoming transfers will be blocked.                               |

When you freeze a virtual account:

1. Tendar sends a freeze request to Focus MFB.
2. The `freeze_status` is updated to `active` in Tendar's database.
3. Any subsequent transfer attempts to the account will be rejected by the bank.

When you unfreeze:

1. Tendar sends an unfreeze request to Focus MFB.
2. The `freeze_status` is updated back to `inactive`.
3. The account can receive transfers again.

Freezing is reversible and non-destructive — the account number doesn't change, the account isn't deleted, and any existing balance remains intact. Think of it as a pause button.

{% hint style="warning" %}
You can't freeze an account that's already frozen, and you can't unfreeze an account that isn't frozen. Attempting either will return a `400 Bad Request` error with a descriptive message.
{% endhint %}

***

## The virtual account object

Here's the complete breakdown of every field on a virtual account:

| Field               | Type          | Description                                                                                                           |
| ------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------- |
| `id`                | string        | Tendar's internal ID for the virtual account.                                                                         |
| `user`              | object/string | The user linked to this account. Populated as a full user object when fetching, or as an ID string in list responses. |
| `user_id`           | string        | The Tendar user ID (e.g., `usr-hnlfkizfoc`). Empty if the account was created without a `user_id`.                    |
| `account_number`    | string        | The bank account number (e.g., `1100017689`). This is the number customers transfer to.                               |
| `first_name`        | string        | The account holder's first name.                                                                                      |
| `last_name`         | string        | The account holder's last name.                                                                                       |
| `phone_number`      | string        | The account holder's phone number in E.164 format.                                                                    |
| `gender`            | string        | The account holder's gender (`male` or `female`).                                                                     |
| `place_of_birth`    | string        | The account holder's place of birth.                                                                                  |
| `date_of_birth`     | string        | The account holder's date of birth (`DD/MM/YYYY`).                                                                    |
| `email`             | string        | The account holder's email address.                                                                                   |
| `address`           | string        | The account holder's residential address.                                                                             |
| `account_tier`      | string        | The KYC tier of the account (e.g., `1`). Determines transaction and balance limits.                                   |
| `status`            | string        | The account status (e.g., `active`). Reflects the real-time status at Focus MFB when fetched via the detail endpoint. |
| `metadata`          | object        | Custom key-value pairs attached to the account.                                                                       |
| `available_balance` | number        | The available balance (only present when fetching account details).                                                   |
| `ledger_balance`    | number        | The ledger balance (only present when fetching account details).                                                      |
| `service_provider`  | string        | The banking provider (`focus_mfb`).                                                                                   |
| `freeze_status`     | string        | Whether the account is frozen: `inactive` (not frozen) or `active` (frozen).                                          |

***

## Putting it all together

Here's a complete integration example — from creating a virtual account to receiving a payment notification.

{% stepper %}
{% step %}

### Create the virtual account

```bash
curl -X POST https://api.tendar.io/api/v1/virtual-account/create \
  -H "Authorization: Bearer sk_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "John",
    "last_name": "Doe",
    "phone_number": "+2348181765499",
    "gender": "male",
    "place_of_birth": "Lagos",
    "date_of_birth": "15/03/1995",
    "email": "john@example.com",
    "bvn": "22549535647",
    "address": "26, Mabinuori Street, Ikosi, Lagos",
    "metadata": {
      "loan_id": "loan-4829"
    }
  }'
```

Save the `account_number` from the response — that's what you'll share with the customer.
{% endstep %}

{% step %}

### Share the account number with the customer

Display the account number to your customer however fits your product. For example:

```
To make your loan repayment, transfer the amount to:

  Bank:           Focus MFB (via any bank)
  Account Number: 1100017689
  Account Name:   John Doe

Transfers from any Nigerian bank will be automatically detected.
```

{% endstep %}

{% step %}

### Customer makes a transfer

The customer opens their banking app — GTBank, Access, FirstBank, any bank — and transfers ₦50,000 to account `1100017689`. This is a standard NIP transfer. The customer doesn't need to install any app, visit any website, or do anything outside their normal banking routine.
{% endstep %}

{% step %}

### Listen for the webhook

Your webhook endpoint receives a `payment.success` event:

```json
{
    "company_id": "64245949dbcc9f021a769e65",
    "event": "payment.success",
    "service": "recollection",
    "data": {
        "id": "692d1f3a1e4102c1fda719f7",
        "user_id": "usr-hnlfkizfoc",
        "email": "john@example.com",
        "payment_channel": "virtual_account",
        "provider": "focus_mfb",
        "amount": 50000,
        "fees": 1000,
        "currency": "NGN",
        "reference": "FT2025120100001234",
        "status": "success",
        "account_number": "1100017689",
        "paid_at": "2025-12-01T14:22:31Z",
        "metadata": {
            "loan_id": "loan-4829"
        }
    }
}
```

At this point, you can:

* Record the loan repayment against `loan-4829`.
* Notify the customer via SMS or email that their payment was received.
* Update the customer's account balance in your system.
  {% endstep %}

{% step %}

### Check the account balance

If you want to verify the account state after the transfer:

```bash
curl https://api.tendar.io/api/v1/virtual-account/balance-enquiry/1100017689 \
  -H "Authorization: Bearer sk_live_xxxxxxxx"
```

{% endstep %}
{% endstepper %}

***

## Error handling

Here are the most common errors you might encounter:

| Error                                     | HTTP Status | Cause                                                                                                                       | Resolution                                                              |
| ----------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `user not found`                          | 404         | The `user_id` doesn't exist in your company's user directory.                                                               | Verify the user ID or create the user first.                            |
| `account_reference_number is not allowed` | 400         | You included an `account_reference_number` in the request.                                                                  | Remove the field. Account reference numbers are managed internally.     |
| `virtual account not found`               | 404         | The account number doesn't exist or doesn't belong to your company.                                                         | Double-check the account number.                                        |
| `virtual account is already frozen`       | 400         | You tried to freeze an account that's already frozen.                                                                       | Check the `freeze_status` before freezing.                              |
| `virtual account is not frozen`           | 400         | You tried to unfreeze an account that isn't frozen.                                                                         | Check the `freeze_status` before unfreezing.                            |
| Validation errors                         | 400         | Required fields are missing or in the wrong format (e.g., BVN isn't 11 digits, date isn't `DD/MM/YYYY`, phone isn't E.164). | Check the error message for the specific field and format requirements. |
| Subscription/charge errors                | 402/400     | Your subscription doesn't support virtual account creation, or the charge couldn't be initiated.                            | Verify your subscription plan includes virtual accounts.                |

***

## Security considerations

### BVN handling

The customer's BVN is required for account creation (it's a regulatory requirement for opening bank accounts in Nigeria), but it's never returned in API responses. Tendar stores BVNs securely and only transmits them to Focus MFB during account provisioning. You should apply the same care to BVNs in your own system — collect them over HTTPS, don't log them in plain text, and don't store them longer than necessary.

### Validate webhook payloads

When you receive a `payment.success` webhook for a virtual account payment, verify that:

* The `company_id` matches your account.
* The `account_number` corresponds to a virtual account you actually created.
* The `amount` is consistent with what you expect (e.g., a loan repayment amount).

This prevents spoofed webhooks from tricking your system into crediting phantom payments.

### Account freezing for risk management

If you detect suspicious activity on a customer's virtual account — unusually large transfers, transfers from flagged accounts, or patterns that don't match the customer's profile — freeze the account immediately and investigate. Freezing is instant and reversible, so there's no downside to being cautious.

***

## When to use Virtual Accounts vs. other collection methods

Virtual accounts are ideal when the customer prefers to pay via bank transfer on their own schedule. But they're not always the best fit. Here's how they compare to other collection methods:

| Scenario                                                              | Recommended method                                                                      |
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Customer pays via bank transfer, on their own time                    | **Virtual Accounts** (this guide)                                                       |
| One-time payment where the customer is present (checkout flow)        | [Accept Payments](https://docs.tendar.co/documentation/recollection/broken-reference)   |
| Recurring charges on a saved card (subscriptions, loan repayments)    | [Card Tokenization](https://docs.tendar.co/documentation/recollection/broken-reference) |
| Recurring debits from a bank account (loan repayments, subscriptions) | [Direct Debit](https://docs.tendar.co/documentation/recollection/broken-reference)      |
| Automated, mandate-based collection plans with installments           | [Collect](https://docs.tendar.co/documentation/recollection/broken-reference)           |
| Recording a loan repayment (manual or provider-backed)                | [Recollect Loan](https://docs.tendar.co/documentation/recollection/broken-reference)    |

In practice, many lending platforms combine virtual accounts with other methods. For example, you might set up a direct debit for scheduled repayments but also create a virtual account so the customer can make extra payments via bank transfer whenever they want.

***

## What to read next

| Guide                                                                                   | What you'll learn                                                             |
| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [Accept Payments](https://docs.tendar.co/documentation/recollection/broken-reference)   | Initialize one-time payments with a hosted checkout page.                     |
| [Card Tokenization](https://docs.tendar.co/documentation/recollection/broken-reference) | Save a customer's card and charge it repeatedly.                              |
| [Direct Debit](https://docs.tendar.co/documentation/recollection/broken-reference)      | Set up bank account authorizations for recurring debits.                      |
| [Collect](https://docs.tendar.co/documentation/recollection/broken-reference)           | Automate installment-based collections with mandates and scheduled debits.    |
| [Recollect Loan](https://docs.tendar.co/documentation/recollection/broken-reference)    | Record loan repayments — manually or through a saved payment method.          |
| [Overview](https://docs.tendar.co/documentation/recollection/broken-reference)          | The full Recollection Service overview with all available collection methods. |
