> ## 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

> Open a virtual account for one of your users.

The account is opened in the background. A `201` means the request was accepted, not that the account can take money: it comes back without deposit details, and the bank assigns them afterwards. Wait for the `virtual_account.activated` webhook — see [Webhooks](/webhooks/overview) — then read the deposit details off the account.



## 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: Recipients
  - name: Quotations
  - name: Payouts
  - name: Reference
paths:
  /v1/virtual-accounts:
    post:
      tags:
        - Virtual Accounts
      summary: Create a virtual account
      description: >-
        Open a virtual account for one of your users.


        The account is opened in the background. A `201` means the request was
        accepted, not that the account can take money: it comes back without
        deposit details, and the bank assigns them afterwards. Wait for the
        `virtual_account.activated` webhook — see [Webhooks](/webhooks/overview)
        — then read the deposit details off the account.
      operationId: post_v1-virtual-accounts
      parameters:
        - in: header
          name: X-Api-Version
          required: false
          description: >-
            Version applied to this request. It wins over your account's pinned
            version — see [Versioning](/using-the-api/versioning).
          schema:
            type: string
            example: '2026-04-14'
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            format: uuid
            example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
          description: >-
            A UUID you generate for this call. Retrying with the same key
            returns the first account instead of opening a second one; reusing
            it with a different body is rejected with a `409`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateVirtualAccountRequest'
            examples:
              fiat:
                summary: A USD-balance account
                value:
                  user_id: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
                  type: US_BANK
                  bank: austin_capital_trust
                  mode: fiat
                  description: Operating account
              crypto:
                summary: An account that converts deposits to USDC
                value:
                  user_id: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
                  type: US_BANK
                  bank: austin_capital_trust
                  mode: crypto
                  destination:
                    currency: USDC
                    network: polygon
                    address: '0x0000000000000000000000000000000000000001'
                  metadata:
                    ledger_ref: op-4471
      responses:
        '201':
          description: The account was accepted and is being opened.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateVirtualAccountResponse'
              examples:
                fiat:
                  summary: A USD-balance account
                  value:
                    id: 11111111-2222-3333-4444-555555555555
                    status: approved
                    type: US_BANK
                    bank: austin_capital_trust
                    mode: fiat
                    destination: null
                    source_deposit_instructions: null
                    description: Operating account
                    created_at: '2026-09-01T12:00:00.000Z'
                    metadata: {}
                crypto:
                  summary: An account that converts deposits to USDC
                  value:
                    id: 66666666-7777-8888-9999-aaaaaaaaaaaa
                    status: approved
                    type: US_BANK
                    bank: austin_capital_trust
                    mode: crypto
                    destination:
                      currency: USDC
                      network: polygon
                      address: '0x0000000000000000000000000000000000000001'
                    source_deposit_instructions: null
                    created_at: '2026-09-02T09:30:00.000Z'
                    metadata:
                      ledger_ref: op-4471
        '400':
          description: >-
            The request was rejected. Three different bodies arrive with this
            status, so read `error` before anything else:


            - **`Invalid request data`** — the `Idempotency-Key` header is
            missing or is not a UUID.

            - **`Invalid data`** — a field in the body is missing, or is not one
            of the values it accepts. `details` names each problem.

            - **`Bad Request`** — the body was fine but the account cannot be
            opened: the user is not `VERIFIED`, is not a business (`code:
            business_only`), the bank no longer opens accounts (`code:
            bank_not_accepting_new_accounts`), or the bank is one your account
            is not authorized for.
          content:
            application/json:
              examples:
                header:
                  summary: The Idempotency-Key header is not a UUID
                  value:
                    error: Invalid request data
                    details:
                      - path: idempotency-key
                        message: Idempotency-Key must be a valid UUID
                        code: invalid_string
                body:
                  summary: A field in the body is missing or wrong
                  value:
                    code: validation_error
                    error: Invalid data
                    details:
                      - message: user_id is Invalid uuid
                user-not-verified:
                  summary: The user has not finished verification
                  value:
                    statusCode: 400
                    error: Bad Request
                    message: >-
                      User must be in VERIFIED status to create a virtual
                      account. Current status: VERIFYING
                    timestamp: '2026-09-01T12:00:00.000Z'
                not-a-business:
                  summary: The user is not a business
                  value:
                    statusCode: 400
                    error: Bad Request
                    message: >-
                      Virtual accounts are only available for business
                      sub-clients.
                    timestamp: '2026-09-01T12:00:00.000Z'
                    code: business_only
        '403':
          description: >-
            Your request was turned away before it reached the API. The message
            talks about routing, but any of these causes it:


            - The `x-api-key` header is missing or wrong.

            - Your bearer token has expired. Get a new one from [Get access
            token](/api-reference/authentication/get-access-token).

            - The path or the HTTP method does not match. In sandbox, check that
            the path sits under the `/sandbox` prefix.


            A missing `Authorization` header comes back as a `401` with the same
            body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: invalid_request
                message: >-
                  The request could not be matched to a valid route, or was
                  malformed. Verify the path and HTTP method against the API
                  reference. This is a routing or request error, not a
                  credentials or signature problem.
        '404':
          description: >-
            No user with that `user_id` belongs to your account. Check the id,
            and that you are calling the environment the user was created in.
          content:
            application/json:
              schema:
                type: object
                properties:
                  statusCode:
                    type: integer
                    description: Repeats the HTTP status.
                  error:
                    type: string
                    description: The status name.
                  message:
                    type: string
                    description: What went wrong, in one sentence.
                  timestamp:
                    type: string
                    description: When the call was rejected, as an ISO 8601 timestamp.
                  code:
                    type: string
                    description: >-
                      A short machine-readable reason. Only some rejections
                      carry one.
              example:
                statusCode: 404
                error: Not Found
                message: >-
                  User with ID aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee not found or
                  does not belong to client
                timestamp: '2026-09-01T12:00:00.000Z'
        '409':
          description: >-
            The `Idempotency-Key` clashes with an earlier request. Either you
            reused it with a different body, or the first call is still running
            — retry that one with the same body instead of a new key.
          content:
            application/json:
              schema:
                type: object
                properties:
                  statusCode:
                    type: integer
                    description: Repeats the HTTP status.
                  error:
                    type: string
                    description: The status name.
                  message:
                    type: string
                    description: What went wrong, in one sentence.
                  timestamp:
                    type: string
                    description: When the call was rejected, as an ISO 8601 timestamp.
                  code:
                    type: string
                    description: >-
                      A short machine-readable reason. Only some rejections
                      carry one.
              examples:
                different-body:
                  summary: The same key was reused with a different body
                  value:
                    statusCode: 409
                    error: Conflict
                    message: >-
                      Idempotency key has already been used with different
                      request data
                    timestamp: '2026-09-01T12:00:00.000Z'
                in-flight:
                  summary: The first call has not finished
                  value:
                    statusCode: 409
                    error: Conflict
                    message: Request is already being processed
                    timestamp: '2026-09-01T12:00:00.000Z'
        '422':
          description: >-
            The user is verified but not yet eligible for this account.


            When something on the user is missing, `code` is
            `missing_required_fields` and `missing_fields` names exactly what to
            supply — add it to the user and call again with the same
            `Idempotency-Key`. Other refusals carry no `code` and the `message`
            is the reason.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                    description: >-
                      `missing_required_fields` when the user is incomplete.
                      Absent on other refusals.
                  message:
                    type: string
                    description: Why the account cannot be opened.
                  missing_fields:
                    type: array
                    items:
                      type: string
                    description: >-
                      The fields to supply on the user. Only sent alongside
                      `missing_required_fields`.
              example:
                code: missing_required_fields
                message: >-
                  User is not eligible for US_BANK: Missing required fields.
                  Provide the missing fields on the user before creating a
                  virtual account.
                missing_fields:
                  - expected_monthly_volume
                  - expected_transaction_count
      security:
        - bearerAuth: []
          apiKeyAuth: []
      servers:
        - url: https://api.balampay.com/sandbox
          description: Sandbox
        - url: https://api.balampay.com
          description: Production
components:
  schemas:
    CreateVirtualAccountRequest:
      type: object
      description: >-
        Send `bank` or `provider` — one of the two is required. Everything else
        is optional.
      properties:
        user_id:
          type: string
          format: uuid
          description: >-
            The user the account belongs to. The user has to be a business with
            `status: VERIFIED` and be eligible for the product, or the call is
            refused.
        type:
          type: string
          enum:
            - US_BANK
          description: Account type.
        bank:
          type: string
          enum:
            - austin_capital_trust
            - jp_morgan
          description: >-
            Which bank the account runs on, and with it the rail — see [Virtual
            account values](/reference/virtual-accounts/values#bank). Required
            unless you send `provider` instead.


            A bank your account is not authorized for is rejected with `Invalid
            bank`.
        provider:
          type: string
          enum:
            - act
            - jp_morgan
          description: >-
            A short name for a bank: `act` means `austin_capital_trust`, and
            `jp_morgan` means the bank of the same name. Send this instead of
            `bank`, not as well.
        mode:
          type: string
          enum:
            - fiat
            - crypto
          description: >-
            What the account does with a deposit, and it cannot be changed
            later: `fiat` keeps it as a USD balance, `crypto` converts it and
            sends it to `destination`. See [Virtual account
            values](/reference/virtual-accounts/values#mode).


            Omit it and a `destination` means `crypto`, none means `fiat`.
            `fiat` with a `destination` is rejected.
        destination:
          type: object
          description: >-
            Where a converted deposit is sent. Omit it for a `fiat` account.


            You can also open a `crypto` account without one — the address is
            then created on the first payout.
          properties:
            currency:
              type: string
              enum:
                - USDC
                - USDT
              description: The token a deposit is converted to.
            network:
              type: string
              enum:
                - polygon
                - solana
                - tron
              description: The chain the token is sent on.
            address:
              type: string
              minLength: 1
              description: >-
                The wallet the token is sent to. It has to match the chain: `0x`
                and 40 hexadecimal characters on `polygon`, 32 to 44 Base58
                characters on `solana`, `T` and 33 Base58 characters on `tron`.
          required:
            - currency
            - network
            - address
        description:
          type: string
          description: Your own label for the account. It comes back on every read.
        metadata:
          type: object
          additionalProperties:
            type: string
          description: >-
            Your own key/value pairs, returned unchanged. Up to 50 keys. A key
            is 1 to 40 characters and cannot contain `[` or `]`; a value is up
            to 500 characters.
        markup:
          type: object
          description: >-
            Your fees on this account, each a decimal string such as `"1.50"`.
            Negative values are rejected, and so is any key not listed here. Ask
            your Kira contact before setting these.
          properties:
            wire_fixed_fee:
              type: string
              description: Flat fee on an inbound wire.
            ach_fixed_fee:
              type: string
              description: Flat fee on an inbound ACH transfer.
            inbound_variable_fee_pct:
              type: string
              description: Percentage fee on an inbound deposit.
            usd_usdc_conversion_fee_pct:
              type: string
              description: Percentage fee on converting USD to a token.
          additionalProperties: false
      required:
        - user_id
        - type
    CreateVirtualAccountResponse:
      type: object
      description: >-
        The account as it exists right after the call. Deposit details are not
        assigned yet, so `source_deposit_instructions` is `null` on every new
        account.
      properties:
        id:
          type: string
          format: uuid
          description: Virtual account UUID. Use it to read the account later.
        status:
          type: string
          description: >-
            Where the account is in its lifecycle — see [Virtual account
            values](/reference/virtual-accounts/values#status). A new account
            always comes back as `approved`, which means it was accepted, not
            that it can take money yet.
        type:
          type: string
          enum:
            - US_BANK
          description: Account type.
        bank:
          type: string
          description: >-
            Which bank the account runs on — see [Virtual account
            values](/reference/virtual-accounts/values#bank). When you sent
            `provider`, this is the bank it stands for.
        mode:
          type: string
          description: >-
            What the account does with a deposit. Available options: `fiat`,
            `crypto`. See [Virtual account
            values](/reference/virtual-accounts/values#mode).
        destination:
          type:
            - object
            - 'null'
          description: >-
            The `destination` you sent, returned unchanged. `null` when you sent
            none.
          properties:
            currency:
              type: string
              description: The token a deposit is converted to.
            network:
              type: string
              description: The chain the token is sent on.
            address:
              type: string
              description: The wallet the token is sent to.
        source_deposit_instructions:
          type:
            - object
            - 'null'
          description: >-
            The bank details a payer sends to. Always `null` here — the bank
            assigns them while it opens the account.
          additionalProperties: true
        description:
          type: string
          description: The label you sent. Absent when you sent none.
        markup:
          type: object
          description: The fees you sent, returned unchanged. Absent when you sent none.
          additionalProperties: true
        created_at:
          type: string
          description: When the account was created, as an ISO 8601 timestamp.
        metadata:
          type: object
          description: >-
            The key/value pairs you sent, returned unchanged. `{}` when you sent
            none.
          additionalProperties: true
    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: >-
        The `data.access_token` value from [Get access
        token](/api-reference/authentication/get-access-token).
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API key issued by Kira.

````