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

# 🏛️ Virtual Accounts

> Give your US users a named bank account of their own, so funds arrive under their name instead of a shared account.

***

## Overview

A **virtual account** is a US bank account — account number plus ABA routing number — issued in your **user's own name**. Funds sent to it land directly in that user's balance, with no shared account and no manual reference matching.

Virtual accounts are provisioned through **banking onboarding**: Capa submits the user's verified identity to our US banking partner, the partner runs its own review, and on approval the account is created and attached to the user.

<Note>
  Virtual accounts are currently available for the **United States (`US`)** only. `country` must be `US` on both endpoints — any other value is rejected with `400`.
</Note>

## Before You Start

The user must have **completed KYB** with Capa. Banking onboarding does not collect identity data of its own — it consumes what KYB already produced, so a user who has cleared KYB is ready to be onboarded. See [Create and Verify a User](/docs/create-and-verify-a-user-with-capa-api).

## The Flow

<Steps>
  <Step title="Check eligibility">
    Call [Get Virtual Account Status](/reference/bankingonboardingpartnerv2controller_getonboardingstatus) to see whether the user can be onboarded.

    ```
    GET /api/partner/v2/banks/users/{userId}/banking-onboarding?country=US
    ```

    ```json theme={null}
    {
      "success": true,
      "data": {
        "hasVirtualAccount": false,
        "canRequest": true,
        "status": "NOT_STARTED",
        "missingRequirements": []
      }
    }
    ```

    Use `canRequest` to gate the action in your UI. It is `true` only when KYB is complete, the user has no existing virtual account, and no onboarding is already in flight.
  </Step>

  <Step title="Request the account">
    Call [Request Virtual Account](/reference/bankingonboardingpartnerv2controller_requestonboarding).

    ```
    POST /api/partner/v2/banks/users/{userId}/banking-onboarding
    ```

    ```json theme={null}
    { "country": "US" }
    ```

    Branch on `data.status` — a `201` does not always mean a new onboarding was started:

    | `status`               | Meaning                                                    | What to do                               |
    | ---------------------- | ---------------------------------------------------------- | ---------------------------------------- |
    | `SUBMITTED`            | Sent to the banking partner.                               | Poll for completion.                     |
    | `ALREADY_IN_PROGRESS`  | One is already running.                                    | Poll for completion.                     |
    | `ALREADY_COMPLETED`    | The user already has an account.                           | Read it from the bank accounts endpoint. |
    | `REQUIREMENTS_MISSING` | The user's KYB is not complete; **nothing was submitted**. | Finish KYB, then retry.                  |

    The endpoint is safe to call repeatedly — it returns the existing onboarding rather than starting a second one.
  </Step>

  <Step title="Complete KYB if it is still pending">
    `status: "REQUIREMENTS_MISSING"` means the user's KYB is not complete. Send them to `kybLink` to finish it:

    ```json theme={null}
    {
      "success": true,
      "data": {
        "status": "REQUIREMENTS_MISSING",
        "kybLink": "https://verify.capa.fi/s/2f9c1b7e",
        "missingRequirements": [
          { "field": "taxId", "message": "Tax ID is required for business onboarding" }
        ]
      }
    }
    ```

    `missingRequirements` lists what is still outstanding — useful for surfacing a reason in your UI, but you do not need to collect those fields yourself. Completing KYB satisfies them. `kybLink` is optional; if it is absent, direct the user through your usual KYB flow.

    Once KYB is complete, call the request endpoint again.
  </Step>

  <Step title="Poll until completion">
    There is **no webhook** for virtual account onboarding. Poll the status endpoint until `status` is `COMPLETED` or `FAILED`.

    | `status`      | Meaning                                                                 |
    | ------------- | ----------------------------------------------------------------------- |
    | `NOT_STARTED` | No onboarding has ever been created.                                    |
    | `IN_PROGRESS` | Submitted, awaiting the banking partner's decision.                     |
    | `COMPLETED`   | Approved and provisioned.                                               |
    | `FAILED`      | Did not complete. `canRequest` returns to `true`, so it can be retried. |

    Approval typically takes minutes to hours, so poll every few minutes — not every few seconds.
  </Step>

  <Step title="Read the account details">
    Once `hasVirtualAccount` is `true`, fetch the account from [List Bank Accounts](/reference/userbankinfopartnerv2controller_getuserbankinfosbyuserid):

    ```
    GET /api/partner/v2/banks/users/{userId}/accounts?country=US
    ```

    The provisioned account appears as a normal bank account record carrying the user's `accountNumber`, `routingNumber`, `bankName`, and `accountType`. It is stored with `rampType: "CROSS_RAMP"`, so you can filter on that to isolate virtual accounts from bank accounts the user added themselves.
  </Step>
</Steps>

## Retrying a Failed Onboarding

A `FAILED` onboarding is not terminal. `canRequest` flips back to `true`, and calling the request endpoint again starts a fresh attempt. If the user's KYB is not complete, the retry returns `REQUIREMENTS_MISSING` rather than failing again.

***

## What's Next

* [Request Virtual Account (User)](/reference/bankingonboardingpartnerv2controller_requestonboarding)
* [Get Virtual Account Status (User)](/reference/bankingonboardingpartnerv2controller_getonboardingstatus)
* [🏦 Bank Accounts](/docs/bank-accounts)
