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

# Creating a virtual account

> How to issue a virtual account for a customer, follow it to ACTIVE, and receive the first deposit as stablecoin.

A virtual account gives your customer a set of bank details to receive fiat into. When a payment lands, Fin.com converts it and settles the proceeds as stablecoin to a wallet address you nominate.

This guide covers the whole path: what to have in place first, how to create the account, how to follow it to `ACTIVE`, and what to expect on the first deposit.

## Before you start

* **An approved customer.** Virtual accounts belong to a customer, so the customer has to exist and have reached `APPROVED`. See [Onboarding individual customers](/guides/customers/onboarding-individuals) or [Onboarding business customers](/guides/customers/onboarding-businesses).
* **Confirm your customer has the capability.** Call [Get Customer Capabilities](/api-reference/customers/get-customer-capabilities) and check the `on_ramp` entry for the currency and rail you intend to use. A customer can be approved without being eligible for every corridor.
* **A destination wallet that you have custody of.** The converted funds settle to an address you nominate, and Fin.com does not custody them for you. Have the address ready on the network you intend to use, and the means to sign transactions out of it.

## Choosing the source and destination

A virtual account has two sides. The **source** is the fiat side, meaning the bank details your customer hands out. The **destination** is the crypto side, meaning where the converted funds settle.

| Side        | Field      | Available now             |
| :---------- | :--------- | :------------------------ |
| Source      | `currency` | `USD`                     |
| Source      | `rail`     | `ACH`, `FEDWIRE`, `SWIFT` |
| Source      | `bank`     | `SSB`, `PORTAGE`          |
| Destination | `currency` | `USDC`                    |
| Destination | `rail`     | `POLYGON`, `ETHEREUM`     |

<Note>
  This guide covers issuing USD virtual accounts only.
</Note>

Not every combination is valid even within these values. Check [Supported rails and currencies](/guides/others/supported-rails-and-currencies) before you build against a pair, and note that what is enabled for your account may be narrower still.

The `destination.wallet` format follows the network: Ethereum, Base and Polygon take a hexadecimal address, Solana takes base58. Check it carefully. Funds settled to a wrong address cannot be recovered.

## Creating the account

Call [Create Virtual Account](/api-reference/virtual-accounts/create-virtual-account-v3) with the customer in the path.

```json theme={null}
POST /v3/customers/{customer_id}/virtual-accounts

{
  "destination": {
    "wallet": "0x7A3f5C21b9E04d8a6C15fB3e920D74Ac815b6E39",
    "currency": "USDC",
    "rail": "ETHEREUM"
  },
  "source": {
    "currency": "USD",
    "rail": "FEDWIRE",
    "bank": "SSB"
  },
  "developer_fee": {
    "fixed": 0,
    "percentage": 0
  }
}
```

`developer_fee` is your own markup on each deposit. Set a fixed amount, a percentage, or both, and both are applied.

### What comes back

A `200` with the account in `data`. The field to read first is `status`, and on a fresh account it is `PROCESSING`:

```json theme={null}
{
  "data": {
    "id": "b84f17e1-96a8-4034-8394-e902ed403d96",
    "status": "PROCESSING",
    "deposit_instructions": null,
    "destination": {
      "address": "0x7A3f5C21b9E04d8a6C15fB3e920D74Ac815b6E39",
      "currency": "USDC",
      "destination_chain": "ETHEREUM"
    },
    "bank": "SSB",
    "rfi": null
  }
}
```

<Warning>
  `deposit_instructions` is `null` until the banking partner issues the account. Do not treat a `200` as the account being ready, and do not send a customer anywhere to pay until you have the deposit instructions in hand.
</Warning>

### One account per currency and rail

A customer can hold only one virtual account per source currency and rail. Requesting one that already exists returns `422`, with the colliding `source` in `errors` and the `virtual_account_id` of the existing account:

```json theme={null}
{
  "errors": [
    {
      "customer_id": "c1f4a8e2-3b57-4d09-9a61-7e2b5c8d4f30",
      "source": {
        "currency": "USD",
        "rail": "FEDWIRE"
      }
    },
    {
      "virtual_account_id": "9d2b7f41-6c8a-45e3-b071-2fa93c5e8d16"
    }
  ],
  "message": "Virtual account already exists for the given parameters."
}
```

Use the account it points at rather than retrying. Treat `422` here as a lookup result, not a failure.

## Waiting for the account to go live

New accounts are reviewed before the banking partner issues them. The account moves through these states:

| Status                    | Meaning                                                                        |
| :------------------------ | :----------------------------------------------------------------------------- |
| `PROCESSING`              | Submitted and under review                                                     |
| `IN_COMPLIANCE`           | Under compliance review                                                        |
| `REQUEST_FOR_INFORMATION` | More information is needed. The `rfi` object says what                         |
| `ACTIVE`                  | Issued. `deposit_instructions` are populated and the account can receive money |
| `INACTIVE`                | No longer usable                                                               |
| `DECLINED`                | Not issued                                                                     |

### Which webhooks to subscribe to

Subscribe to both:

| Event                                                                              | Fires when                                               |
| :--------------------------------------------------------------------------------- | :------------------------------------------------------- |
| [`virtual_account.created.v2`](/api-reference/webhooks/virtual-account-created-v2) | The account record is created, while still `PROCESSING`  |
| [`virtual_account.status.v2`](/api-reference/webhooks/virtual-account-status-v2)   | The status changes, including the transition to `ACTIVE` |

`virtual_account.status.v2` is the one that matters operationally. It carries the full account object, so when `status` becomes `ACTIVE` the same payload contains the populated `deposit_instructions`. You do not need a follow-up call to fetch them.

```json theme={null}
{
  "data": {
    "id": "ed96d65a-5fb1-49f2-8715-0c6aa32220ca",
    "rfi": null,
    "status": "ACTIVE",
    "created_at": "2026-03-17T14:27:49.073891Z",
    "updated_at": "2026-03-17T14:42:55.788231316Z",
    "customer_id": "c3088a8d-50de-48cf-a137-8d46815394f8",
    "destination": {
      "address": "0x7A3f5C21b9E04d8a6C15fB3e920D74Ac815b6E39",
      "currency": "USDC",
      "destination_chain": "ETHEREUM"
    },
    "developer_fee_fixed": 0.22,
    "deposit_instructions": {
      "currency": "USD",
      "bank_code": {
        "code": "SSBAUS32",
        "type": "SWIFT"
      },
      "bank_name": "SSB Bank",
      "account_type": "BankSwift",
      "bank_address": null,
      "bank_country": "USA",
      "payment_rails": [
        "ACH"
      ],
      "bank_account_number": "235464829825",
      "bank_routing_number": ""
    },
    "developer_fee_percent": 0
  },
  "event": {
    "id": "8c236a87-4ab2-49af-b22f-02d1d8e12cfa",
    "type": "virtual_account.status.v2",
    "created_at": "2026-03-17T14:42:55.813225Z",
    "sandbox_mode": true,
    "event_reference_id": "ed96d65a-5fb1-49f2-8715-0c6aa32220ca"
  }
}
```

<Note>
  `event_reference_id` carries the virtual account ID, so you can match the event to your own record without parsing `data`.
</Note>

### How long it takes

Issuance is not instant and is not a fixed interval. It depends on the banking partner and on the customer's risk profile, and a `REQUEST_FOR_INFORMATION` pauses it until you respond.

Build for this rather than polling tightly:

* **Drive off the webhook.** Treat `ACTIVE` as an event you react to, not a state you wait on inside a request.
* **If you must poll, poll slowly.** [Get Virtual Account Details](/api-reference/virtual-accounts/get-virtual-account-details) returns the same object. Minutes between calls, not seconds.
* **Handle `REQUEST_FOR_INFORMATION`.** The `rfi` object is populated only in this state and tells you what is outstanding. Nothing progresses until it is answered.
* **Do not show a customer any bank details** until you have seen `deposit_instructions` populated.

## Next: receiving the first deposit

The account can now receive money. See [Receiving the first deposit](/guides/virtual-accounts/receiving-the-first-deposit) for what to hand to the payer, what lands at your destination wallet, and how to reconcile it.

## Managing accounts

* [List Virtual Accounts](/api-reference/virtual-accounts/list-virtual-accounts-v3) returns every account for a customer, paginated.
* [Get Virtual Account Details](/api-reference/virtual-accounts/get-virtual-account-details) returns one account, including its deposit instructions.
* [Fetch Virtual Account Transactions](/api-reference/virtual-accounts/fetch-virtual-account-transactions) returns what has been received.

<Note>
  **Need help getting set up?**

  Contact us in your dedicated Slack channel to confirm which banking partners and corridors are enabled for you.
</Note>
