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

> ## Agent Instructions
> Money is always a decimal string at the currency's precision (for example "29.00"), paired with an ISO 4217 currency field. Never use minor units.
> Build against the sandbox first: base URL https://sandbox-api.bachs.io with sk_sandbox_ keys. Production is https://api.bachs.io with sk_live_ keys; going live is a key swap.
> Treat webhooks (for example collection.succeeded) as the source of truth for fulfilment, never client-side events or redirects.
> Subscriptions are created by completing a checkout for a recurring product. There is no direct create-subscription endpoint.
> IDs carry resource prefixes (cust_, prod_, sub_, chk_, inv_, ref_) and timestamps are ISO 8601 UTC.

# Virtual accounts

> Give an account a fixed bank account number. Money sent to it becomes a payment on that account.

A **virtual account** is a fixed bank account number issued to your platform or a connected account. It does not expire, and anyone can send money to it at any time. Each deposit becomes a payment on the account that received it.

An account holds one virtual account per currency. `NGN` is the only currency issued today.

To collect into your own account, use your API key without `X-Account-Id`. To issue a number for a connected account, use your platform's key with that account's ID in `X-Account-Id`. The deposit becomes a payment on whichever account owns the number. See [Acting as an account](/connect/acting-as-an-account).

***

## Turn it on

A virtual account requires an active `virtual_accounts` capability. Request it through the account API, then complete the requirements returned for that account, including the representative's BVN. The requirements can include identity and product information; BVN alone is not the complete checklist.

Your API key needs `connected_accounts:write` to request the capability or submit account requirements, `virtual_accounts:write` to create the number and `virtual_accounts:read` to retrieve it. Existing keys do not gain these scopes automatically. Add the needed scopes through [Edit scopes](/developer-portal/api-keys#editing-scopes).

<Steps>
  <Step title="Request the capability">
    Update your own account or a connected account you own with `virtual_accounts` under the `merchant` configuration. Use the account ID returned by the account API.

    ```bash Request virtual accounts theme={"dark"}
    curl -X POST https://sandbox-api.bachs.io/v1/accounts/acct_7KpQ2mNv4XbR9dLc \
      -H "Authorization: Bearer $BACHS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "configuration": {
          "merchant": {
            "capabilities": { "virtual_accounts": { "requested": true } }
          }
        }
      }'
    ```

    This is an illustrative request, not a recorded API result. Requesting the capability does not establish live approval or issue a number immediately. See [Request a capability](/connect/capabilities#request-a-capability).
  </Step>

  <Step title="Read and complete its requirements">
    Read the account's `requirements.entries[]`, or use `account.updated` to learn when it changes. Submit every field required for `virtual_accounts`, including a representative in `persons` with `relationship.representative: true` and their BVN in `id_numbers` with `type: "bvn"`.

    The BVN belongs to the representative for this account. Use the account's returned requirements to determine the other fields and their status. See [Submitting requirements](/connect/requirements#submitting).
  </Step>

  <Step title="Wait until the capability is active">
    Read the capability's status or listen for `capability.updated` with `data.capability: "virtual_accounts"` and `data.status: "active"`. Sandbox grants can become active immediately and do not prove live approval.
  </Step>

  <Step title="Retrieve or create the number">
    Once the capability is active, check `GET /v1/virtual-accounts` for an existing number. If none exists for NGN, create it with the call below. Repeating creation returns the existing number for the account and currency.
  </Step>
</Steps>

<Note>
  Capability approval and feature availability are separate checks. If creation returns `404 NOT_FOUND` even though the capability is active, contact [support](mailto:support@bachs.io) with the account ID and error. Requesting the capability through the API does not bypass account availability checks.
</Note>

<Note>
  Sandbox numbers are fictitious and cannot receive real bank deposits. Use sandbox to build the request and response handling; it does not test an actual deposit into a bank account.
</Note>

***

## Create the virtual account

`POST /v1/virtual-accounts` · scope `virtual_accounts:write` · [full field reference](/api-reference/virtual-accounts/object)

Name the currency. Calling this a second time with the same currency returns the account you already hold, so a retry after a timeout never leaves you with two numbers.

<CodeGroup>
  ```bash Request theme={"dark"}
  curl -X POST https://sandbox-api.bachs.io/v1/virtual-accounts \
    -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
    -H "Content-Type: application/json" \
    -d '{
      "currency": "NGN"
    }'
  ```

  ```json Response theme={"dark"}
  {
    "id": "va_8Hs2kQ4mZpXv",
    "currency": "NGN",
    "account_number": "9902847361",
    "bank_name": "Example Bank",
    "bank_code": "000",
    "status": "active",
    "created_at": "2026-09-22T09:14:02.000Z"
  }
  ```
</CodeGroup>

This example creates a number for your own account. For a connected account, add `-H "X-Account-Id: acct_3Wq8ZfT1yHnJ5sVe"` to the request and use your platform's API key.

<Info>
  Do not treat a network or `5xx` error on create as proof that no account was made. Call `GET /v1/virtual-accounts` before retrying, or retry the create, which returns the account you already hold rather than a second number.
</Info>

## Read it back

`GET /v1/virtual-accounts` · scope `virtual_accounts:read`

Returns the same object shown above for one currency. The currency defaults to `NGN` when you omit it. You get `404` with `NOT_FOUND` when the account has no virtual account in that currency. Add `X-Account-Id` to read a connected account's number.

```bash Request theme={"dark"}
curl "https://sandbox-api.bachs.io/v1/virtual-accounts?currency=NGN" \
  -H "Authorization: Bearer sk_sandbox_abc123xyz..."
```

The response has the same shape as the create response.

## Test the integration

The sandbox requests above let you check creation and retrieval. Use [local webhook testing](/developer-portal/local-testing#send-yourself-an-event) to exercise your `collection.succeeded` handler; a triggered sample event is not a bank deposit and may have no `charge_id`. For an end-to-end test of an incoming virtual-account deposit, ask Bachs for the supported sandbox procedure.

***

## Give the number out

Show `account_number` and `bank_name` together. For a connected account, the sender sees that account's business name in their banking app, not your platform's name.

Nothing is reserved in advance. There is no expected amount or per-order reference for the sender to quote. If you need a number associated with a particular order, use a [bank-transfer checkout](/guides/payments/create-whitelabel-checkout). For a fixed virtual account, use the sender details and narration when the sending bank provides them, and reconcile the deposit against your own records before marking an order paid.

***

## Receive the deposit

A deposit arrives from a bank app, outside your integration. Each deposit becomes its own payment. When it succeeds, you receive [`collection.succeeded`](/guides/webhooks/events/collection-succeeded) with `checkout_id` set to `null`, because no checkout created it. Use the event ID to deduplicate deliveries. If `charge_id` is present, you can retrieve the payment with `GET /v1/payments/{payment_id}`.

The example below is for your own account. To receive events for a connected account at your platform endpoint, set the endpoint's `event_source` to `connect` or `all`; the default `account` receives only your own events. A connected account's event has its ID in both `organization_id` and the top-level `account` field. See [Connect events](/guides/webhooks/overview#connect-events).

```json collection.succeeded theme={"dark"}
{
  "id": "evt_3ab4e0d5d27445cf8a52ab3d8cb8f0b1",
  "type": "collection.succeeded",
  "created_at": "2026-09-22T10:41:18.402Z",
  "organization_id": "acct_7KpQ2mNv4XbR9dLc",
  "data": {
    "charge_id": "ch_1a2b3c4d5e6f",
    "checkout_id": null,
    "reference": null,
    "status": "SUCCEEDED",
    "amount": "250000.00",
    "currency": "NGN",
    "settlement_amount": "249700.00",
    "settlement_currency": "NGN",
    "processing_fee": "300.00",
    "processing_fee_currency": "NGN",
    "fee_bearer": "merchant",
    "product_cart": null,
    "customer": { "id": null },
    "payment_method_details": {
      "type": "bank_transfer",
      "bank_transfer": {
        "sender_name": "JANE ADEYEMI",
        "sender_bank": "Guaranty Trust Bank",
        "sender_account_number": "2294879124",
        "session_id": "000013260922104115000821734502",
        "virtual_account": {
          "id": "va_8Hs2kQ4mZpXv",
          "account_number": "9902847361",
          "bank_name": "Example Bank",
          "type": "permanent",
          "expires_at": null
        }
      }
    },
    "metadata": {}
  }
}
```

`checkout_id`, `reference`, `product_cart`, and `customer.id` are `null` on a deposit into a fixed virtual account. A bank transfer paid through a checkout has a `checkout_id`; `reference` appears if you supplied one, `product_cart` when products were purchased, and `customer.id` when the checkout has a customer.

An NGN virtual account deposit costs 1.5% of the amount received, capped at NGN 300. The fee is deducted before settlement, so the example above settles NGN 249,700 from an NGN 250,000 deposit. See [Fees](/for-you/fees) and [Processing fees](/connect/processing-fees).

The money reaches the account's payout schedule the same way as any other payment. See [Payouts](/connect/payouts).

<Note>
  **A deposit above the account's limit waits before the money reaches its balance.** An account has a limit on one deposit and a limit on a day's deposits added together. A deposit over either one needs a manual review before the money goes into the balance. [Deposit limits](/guides/payments/deposit-limits) explains how limits work.
</Note>

<Note>
  **You cannot refund a deposit.** The payment comes back with `is_refundable: false`, and a refund request against it is rejected with `400`. If you need to return a deposit, contact [support](mailto:support@bachs.io) for the applicable process. Withdrawals documented here are to the account holder's own accounts. The sending bank may omit sender details from `payment_method_details.bank_transfer`, so do not assume the payload is enough to send money back.
</Note>

***

## Errors

These endpoints return the standard [error envelope](/errors). See [Virtual account errors](/api-reference/error-reference#virtual-accounts) for the possible codes and how to resolve them.

***

## Related

* [Capabilities](/connect/capabilities)
* [Requirements](/connect/requirements)
* [Acting as an account](/connect/acting-as-an-account)
* [The virtual account object](/api-reference/virtual-accounts/object)
* [The payment object](/api-reference/payments/object)
* [capability.updated](/guides/webhooks/events/capability-updated)
* [collection.succeeded](/guides/webhooks/events/collection-succeeded)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.