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

# Create a virtual account

> Creates a `US_BANK` virtual account for a `VERIFIED`, product-eligible user. `US_BANK` is the only supported `type` today; choose **fiat** (a USD-balance account funded by inbound wire/ACH) or **crypto** (an on-chain stablecoin deposit address) via `mode`, and select the bank/provider rail with `bank` (or its `provider` alias).

⚠️ **`bank` is REQUIRED for `US_BANK` VAs — including crypto.** Omitting it returns `400 "bank is required for US_BANK virtual accounts"`. (An earlier support answer that said to remove `bank` for crypto was incorrect.)

The bank value is **environment-scoped**:

-   **Sandbox** (this collection): use `slovak_savings_bank` (the international/crypto rail). Sending `portage` here returns `400 "Invalid bank"` — `portage` is only authorized in production.
-   **Production**: the same crypto rail is `portage`.

`bank` selects the **provider/rail**; `mode: "crypto"` + `destination` make it a crypto VA — both are needed, they are not redundant. Note: Tron (`tron`) is accepted on `slovak_savings_bank` in sandbox even though the `destination.network` enum historically documented only `solana | polygon`.

**Production variant** — change one line: `"bank": "portage"` (everything else stays the same). `slovak_savings_bank` and `portage` are both international/crypto-capable banks; the value is just environment-scoped. Precondition: the user must be `status: VERIFIED` and eligible for the product.

**Not-ready marker:** this VA type shows `account_number: null` until provisioned (the `"PENDING-ACT-ACCOUNT"` sentinel is ACT-only). Funds-ready = `account_number` non-null AND != `"PENDING-ACT-ACCOUNT"`.

You can also pass an optional `description` (string) — a human-readable label for the account. When provided, it is echoed back on the create response and on `GET /v1/virtual-accounts/{id}`.

**Fiat vs crypto mode.** Omit `destination` and send `mode: "fiat"` to create a USD-balance account (no automatic crypto conversion); provide a `destination` for crypto mode. Mode is fixed at creation and cannot be changed. `mode: "fiat"` must omit `destination`, and fiat mode is not supported on certain banks (returns `400 "Fiat mode (omitting destination) is not supported for this bank."`).

**Required-for-VA fields (product eligibility).** The user must satisfy the product's pre-submission required fields before a virtual account can be created: `expected_monthly_volume` and `expected_transaction_count` (the sponsor-bank rule), plus every product-specific field and document the user's `missing_fields` map reports for the product on `GET /v1/users/{id}`. If any is absent, this call returns **`422`** with `code: "missing_required_fields"` and a top-level `missing_fields` array naming exactly which are missing — the account is **not** created in a silently-ineligible state and no `virtual_account.created` webhook is emitted. Supply the fields on `POST /v1/users` (bucketed enums that differ for individuals vs businesses — see [Creating users](/guides/creating-users)) or backfill with `PUT /v1/users/{id}`, then retry — the same `idempotency-key` may be reused after a 422.

{/* version-stamp:start — generated by scripts/stamp-versions.ts, do not edit */}

<Update label="2026-04-14">Changed in this version.</Update>

{/* version-stamp:end */}


## OpenAPI

````yaml /openapi/kira-api.2026-04-14.json post /v1/virtual-accounts
openapi: 3.1.0
info:
  title: Kira API
  version: '2026-04-14'
  description: >-
    REST API for users, KYC/KYB verification, virtual accounts, payouts,
    recipients, and webhooks. Every request requires an `x-api-key` header and a
    bearer access token (see Authentication). Pin your account to version
    `2026-04-14` before integrating.
  contact:
    name: Kira API Support
    email: support@kirafin.ai
servers:
  - url: https://api.balampay.com
    description: Production
  - url: https://api.balampay.com/sandbox
    description: Sandbox
security:
  - bearerAuth: []
    apiKeyAuth: []
tags:
  - name: Authentication
  - name: Versioning
  - name: Users
  - name: Virtual Accounts
  - name: Quotations
  - name: Payouts
  - name: Recipients
  - name: Reference
paths:
  /v1/virtual-accounts:
    post:
      tags:
        - Virtual Accounts
      summary: Create a virtual account
      description: >-
        Creates a `US_BANK` virtual account for a `VERIFIED`, product-eligible
        user. `US_BANK` is the only supported `type` today; choose **fiat** (a
        USD-balance account funded by inbound wire/ACH) or **crypto** (an
        on-chain stablecoin deposit address) via `mode`, and select the
        bank/provider rail with `bank` (or its `provider` alias).


        ⚠️ **`bank` is REQUIRED for `US_BANK` VAs — including crypto.** Omitting
        it returns `400 "bank is required for US_BANK virtual accounts"`. (An
        earlier support answer that said to remove `bank` for crypto was
        incorrect.)


        The bank value is **environment-scoped**:


        -   **Sandbox** (this collection): use `slovak_savings_bank` (the
        international/crypto rail). Sending `portage` here returns `400 "Invalid
        bank"` — `portage` is only authorized in production.

        -   **Production**: the same crypto rail is `portage`.


        `bank` selects the **provider/rail**; `mode: "crypto"` + `destination`
        make it a crypto VA — both are needed, they are not redundant. Note:
        Tron (`tron`) is accepted on `slovak_savings_bank` in sandbox even
        though the `destination.network` enum historically documented only
        `solana | polygon`.


        **Production variant** — change one line: `"bank": "portage"`
        (everything else stays the same). `slovak_savings_bank` and `portage`
        are both international/crypto-capable banks; the value is just
        environment-scoped. Precondition: the user must be `status: VERIFIED`
        and eligible for the product.


        **Not-ready marker:** this VA type shows `account_number: null` until
        provisioned (the `"PENDING-ACT-ACCOUNT"` sentinel is ACT-only).
        Funds-ready = `account_number` non-null AND != `"PENDING-ACT-ACCOUNT"`.


        You can also pass an optional `description` (string) — a human-readable
        label for the account. When provided, it is echoed back on the create
        response and on `GET /v1/virtual-accounts/{id}`.


        **Fiat vs crypto mode.** Omit `destination` and send `mode: "fiat"` to
        create a USD-balance account (no automatic crypto conversion); provide a
        `destination` for crypto mode. Mode is fixed at creation and cannot be
        changed. `mode: "fiat"` must omit `destination`, and fiat mode is not
        supported on certain banks (returns `400 "Fiat mode (omitting
        destination) is not supported for this bank."`).


        **Required-for-VA fields (product eligibility).** The user must satisfy
        the product's pre-submission required fields before a virtual account
        can be created: `expected_monthly_volume` and
        `expected_transaction_count` (the sponsor-bank rule), plus every
        product-specific field and document the user's `missing_fields` map
        reports for the product on `GET /v1/users/{id}`. If any is absent, this
        call returns **`422`** with `code: "missing_required_fields"` and a
        top-level `missing_fields` array naming exactly which are missing — the
        account is **not** created in a silently-ineligible state and no
        `virtual_account.created` webhook is emitted. Supply the fields on `POST
        /v1/users` (bucketed enums that differ for individuals vs businesses —
        see [Creating users](/guides/creating-users)) or backfill with `PUT
        /v1/users/{id}`, then retry — the same `idempotency-key` may be reused
        after a 422.
      operationId: post_v1-virtual-accounts
      parameters:
        - in: header
          name: X-Api-Version
          required: false
          description: >-
            Optional. The date-versioned API version to apply for this request
            (e.g. `2026-04-14`). When sent it always wins, even over your pinned
            account default. When omitted, the API uses your account's pinned
            version if set, otherwise a baseline default.
          schema:
            type: string
            example: '2026-04-14'
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            format: uuid
            example: '{{idempotency_key}}'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateVirtualAccountRequest'
            examples:
              create-va-international-fiat:
                summary: Create VA — international fiat
                value:
                  user_id: '{{verified_user_id}}'
                  type: US_BANK
                  mode: fiat
                  bank: slovak_savings_bank
              create-va-usd-fiat-via-act:
                summary: Create VA — USD (fiat, via ACT)
                value:
                  user_id: '{{verified_user_id}}'
                  type: US_BANK
                  provider: act
                  mode: fiat
              create-va-usdc-on-solana-crypto-sandbox-bank-slovak-savings-bank:
                summary: >-
                  Create VA — USDC on Solana (crypto · sandbox bank:
                  slovak_savings_bank)
                value:
                  user_id: '{{verified_user_id}}'
                  type: US_BANK
                  mode: crypto
                  bank: slovak_savings_bank
                  destination:
                    currency: USDC
                    network: solana
                    address: '{{your_solana_wallet}}'
              create-va-usdc-on-polygon-crypto-sandbox-bank-slovak-savings-bank:
                summary: >-
                  Create VA — USDC on Polygon (crypto · sandbox bank:
                  slovak_savings_bank)
                value:
                  user_id: '{{verified_user_id}}'
                  type: US_BANK
                  mode: crypto
                  bank: slovak_savings_bank
                  destination:
                    currency: USDC
                    network: polygon
                    address: '{{your_polygon_wallet}}'
              create-va-usdt-on-tron-crypto-sandbox-bank-slovak-savings-bank:
                summary: >-
                  Create VA — USDT on Tron (crypto · sandbox bank:
                  slovak_savings_bank)
                value:
                  user_id: '{{verified_user_id}}'
                  type: US_BANK
                  mode: crypto
                  bank: slovak_savings_bank
                  destination:
                    currency: USDT
                    network: tron
                    address: '{{your_tron_wallet}}'
              create-va-usdt-on-solana-crypto-sandbox-bank-slovak-savings-bank:
                summary: >-
                  Create VA — USDT on Solana (crypto · sandbox bank:
                  slovak_savings_bank)
                value:
                  user_id: '{{verified_user_id}}'
                  type: US_BANK
                  mode: crypto
                  bank: slovak_savings_bank
                  destination:
                    currency: USDT
                    network: solana
                    address: '{{your_solana_wallet}}'
              create-va-usdt-on-polygon-crypto-sandbox-bank-slovak-savings-bank:
                summary: >-
                  Create VA — USDT on Polygon (crypto · sandbox bank:
                  slovak_savings_bank)
                value:
                  user_id: '{{verified_user_id}}'
                  type: US_BANK
                  mode: crypto
                  bank: slovak_savings_bank
                  destination:
                    currency: USDT
                    network: polygon
                    address: '{{your_polygon_wallet}}'
      responses:
        '201':
          description: Created. Returns the new virtual account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateVirtualAccountResponse'
        '400':
          description: Validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
        '401':
          description: Unauthorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >-
            The user cannot open this virtual account. Two distinct cases, and
            they need different handling:


            - **Missing required fields** (`code: missing_required_fields`) —
            the user is missing `expected_monthly_volume` and/or
            `expected_transaction_count` (the sponsor-bank rule) or another
            product-required field. The response names them in a top-level
            `missing_fields` array; supply them and retry with the same
            `Idempotency-Key`.

            - **Not supported yet** — no `missing_fields` array, because no
            field clears it. The `message` explains the gate; today this is a
            business whose industry requires enhanced due diligence (EDD).
            Retrying will not help. `GET /v1/users/{user_id}` reports the same
            case ahead of time as `eligible_products[].unsupported_reason`, so
            check there before attempting creation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                    example: missing_required_fields
                  message:
                    type: string
                    example: >-
                      Virtual account creation requires expected_monthly_volume
                      and expected_transaction_count. Provide these fields on
                      the user before creating a virtual account.
                  missing_fields:
                    type: array
                    items:
                      type: string
                    example:
                      - expected_monthly_volume
                      - expected_transaction_count
      security:
        - bearerAuth: []
          apiKeyAuth: []
components:
  schemas:
    CreateVirtualAccountRequest:
      type: object
      description: >-
        Request body for `POST /v1/virtual-accounts`. `bank` is required for
        `US_BANK` **unless** `provider` is supplied. `destination` is required
        for crypto mode and must be omitted for `mode: "fiat"`. A
        `Idempotency-Key` (UUID) request header is also required.
      properties:
        user_id:
          type: string
          format: uuid
          description: 'A user with `status: VERIFIED`, eligible for the product.'
        type:
          type: string
          enum:
            - US_BANK
          description: >-
            Virtual-account product. The `US_BANK` path is the documented golden
            path.
        bank:
          type: string
          enum:
            - portage
            - slovak_savings_bank
            - austin_capital_trust
          description: >-
            Selects the provider/rail for `US_BANK`. **Recommended default: the
            international/crypto rail** — `slovak_savings_bank` (sandbox) /
            `portage` (production). **Environment-scoped:** sandbox accepts
            `slovak_savings_bank`, `austin_capital_trust`; production accepts
            `portage`, `austin_capital_trust`. Required for `US_BANK` unless
            `provider` is given.
        provider:
          type: string
          enum:
            - act
          description: >-
            Alias that selects a bank (`act` → `austin_capital_trust`). Provide
            this instead of `bank` to satisfy the bank requirement for
            `US_BANK`.
        mode:
          type: string
          enum:
            - fiat
            - crypto
          description: >-
            Fixed at creation and cannot be changed. `fiat` must omit
            `destination`; `crypto` requires it.
        destination:
          type: object
          description: Crypto payout target. Omit entirely for fiat mode.
          properties:
            currency:
              type: string
              enum:
                - USDC
                - USDT
              description: >-
                Stablecoin auto-converted-to and delivered on the destination
                wallet in crypto mode. Enum is exactly `USDC` | `USDT` (an
                invalid value is rejected with 400 echoing this list). Required
                for crypto mode; omit the whole `destination` object for fiat
                mode.
            network:
              type: string
              enum:
                - solana
                - polygon
                - tron
              description: >-
                Blockchain network the destination wallet address lives on. Enum
                is exactly `polygon` | `solana` | `tron` (an invalid value is
                rejected with 400 echoing this list).
            address:
              type: string
              minLength: 1
              description: >-
                Destination wallet address; validated against the selected
                network's format.
          required:
            - currency
            - network
            - address
        description:
          type: string
          description: >-
            Human-readable label, echoed back on create and on `GET
            /v1/virtual-accounts/{id}`.
        metadata:
          type: object
          additionalProperties:
            type: string
          description: >-
            Stripe-style key/value map: up to 50 keys; key ≤ 40 chars (no `[` or
            `]`); value ≤ 500 chars.
        markup:
          type: object
          description: Optional per-VA fee-markup overrides (decimal strings).
          properties:
            wire_fixed_fee:
              type: string
            ach_fixed_fee:
              type: string
            inbound_variable_fee_pct:
              type: string
            usd_usdc_conversion_fee_pct:
              type: string
          additionalProperties: false
      required:
        - user_id
        - type
    CreateVirtualAccountResponse:
      type: object
      description: Response for `POST /v1/virtual-accounts` (HTTP 201).
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          description: >-
            KYB-mapped status at version `2026-04-14`: typically `approved`,
            `rfi`, or `declined`. `approved` does not mean funds-ready — confirm
            readiness via `account_number` and the `virtual_account.activated`
            webhook.
        type:
          type: string
          enum:
            - US_BANK
          description: >-
            Virtual-account product type — `US_BANK` for the US virtual accounts
            documented in this reference.
        bank:
          type: string
          description: >-
            Present for `US_BANK`; the effective bank (after any `provider`
            alias is resolved).
          enum:
            - portage
            - slovak_savings_bank
            - austin_capital_trust
        mode:
          type: string
          enum:
            - fiat
            - crypto
          description: >-
            Funding mode echoed back. `crypto` accounts return a populated
            `destination` (wallet address); `fiat` accounts return `destination:
            null`.
        destination:
          type:
            - object
            - 'null'
          description: Null for fiat virtual accounts.
          properties:
            currency:
              type: string
            network:
              type: string
            address:
              type: string
              description: >-
                The crypto wallet address (on `destination.network`) that
                converted stablecoin is sent to. Echoes back exactly the address
                sent on create. Present only for crypto mode.
        source_deposit_instructions:
          type:
            - object
            - 'null'
          additionalProperties: true
          description: Provider deposit details; null while the VA is still activating.
        description:
          type: string
          description: Present only when supplied on create.
        markup:
          type: object
          additionalProperties: true
          description: Present only when a non-empty `markup` was supplied on create.
        created_at:
          type: string
          description: ISO 8601.
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Your own key/value metadata, echoed back verbatim. `{test_key:
            "test_value"}` sent on create was returned unchanged.
      required:
        - id
        - status
        - type
        - mode
        - created_at
    ValidationErrorResponse:
      type: object
      description: >-
        Validation / request error. The body shape is **not uniform** across the
        API — it varies by endpoint and by which validation layer rejects the
        request. The fields below are the union of what may appear; treat them
        all as optional. Observed shapes include `{ code, message }`, `{ code,
        message, errors[] }`, `{ code, error, details[] }`, `{ error, details[]
        }`, and a nested `{ error: { code, message, details } }`. Always branch
        on the HTTP status, not on a fixed body shape.
      properties:
        code:
          type: string
        message:
          type: string
        error:
          type:
            - string
            - object
          description: >-
            A short error label (e.g. `"Invalid data"`), or on some endpoints a
            nested `{ code, message, details }` object.
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
            required:
              - field
              - message
        details:
          type:
            - array
            - object
          description: >-
            Per-issue detail. Shape varies by endpoint — typically an array of
            `{ message }` or `{ path, message, code }`, occasionally an object.
    ErrorResponse:
      type: object
      properties:
        message:
          type: string
        code:
          type: string
        statusCode:
          type: number
        error:
          type: string
        timestamp:
          type: string
        path:
          type: string
        details: {}
      required:
        - message
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Access token from `POST /auth` (the `data.access_token` value).
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API key issued by Kira. Required on every request, including `/auth`.

````