> ## 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 the `virtual_accounts` capability and the account representative's BVN. Virtual accounts are rolling out gradually, so contact [hello@bachs.io](mailto:hello@bachs.io) to request access for your platform or a connected account.

Your API key needs `virtual_accounts:write` to create the number and `virtual_accounts:read` to read it (`write` also includes read access). Existing keys do not gain these scopes automatically. Add the needed scope through [Edit scopes](/developer-portal/api-keys#editing-scopes) before calling the endpoints; otherwise the API returns `403`.

<Steps>
  <Step title="Request access">
    Ask Bachs to enable `virtual_accounts` for the account. Include the account ID when you request access for a connected account.
  </Step>

  <Step title="Read what it needs">
    Requesting the capability puts a requirement on the account. Read `requirements.entries[]` on the account, or wait for `account.updated` to tell you it changed. See [Requirements](/connect/requirements).
  </Step>

  <Step title="Submit the representative's BVN">
    Submit it as a `persons` entry with `relationship.representative: true` and an `id_numbers` entry of type `bvn`. This is a requirements submission through the API, the same call you use for every other requirement field. See [Submitting](/connect/requirements#submitting).

    The issuing bank will not create a number without this. It has to be the BVN of the person marked as the account's representative, not any other person on the account.
  </Step>

  <Step title="Wait for approval">
    The capability moves to `active` on review. You are told through the `capability.updated` webhook, with `data.capability` set to `virtual_accounts` and `data.status` set to `active`. See [capability.updated](/guides/webhooks/events/capability-updated).
  </Step>
</Steps>

Approval does not create a number by itself. Once the capability is active, create it with the call below.

***

## 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`. To return the money, send it back as a [payout](/connect/payouts). Confirm the recipient's account number and bank with the sender, including the bank code needed to [register a bank-account payout destination](/api-reference/payouts/create-payout-destination). 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)
