---
title: "Create a virtual account"
method: POST
path: "/virtual_accounts"
tags: ["Virtual Accounts"]
---

# Create a virtual account

`POST /virtual_accounts`

## Request

- Base URL: `https://api.cert.zerohash.com`
- URL: `https://api.cert.zerohash.com/virtual_accounts`
- Auth: API key in header `X-SCX-API-KEY` and API key in header `X-SCX-PASSPHRASE`

Requests a virtual account for a participant. The account is created asynchronously: this endpoint returns `202 Accepted` with `status: PENDING` and no `bank_details`, and the account becomes `ACTIVE` once the banking partner issues it. Poll `GET /virtual_accounts/{id}` for the transition. Idempotency is keyed solely on the required `Idempotency-Key` header, which also becomes the account `id` — replaying the same key returns `200 OK` with the existing record, while a different key against the same participant creates a second, distinct account. An optional `account_label` routes every deposit into the account, and any auto-convert-and-withdraw payout, to that labeled account; it is set once at creation.

## Headers

- `X-SCX-SIGNED` string, required
- `X-SCX-TIMESTAMP` string, required
- `Idempotency-Key` string, uuid, required

## Request body

- CreateVirtualAccountRequest
  - `participant_code` string, required: The participant the virtual account is issued to. The platform is taken from the authenticated API key and must not be supplied.
  - `settlement_policy` union, required: Determines what happens to money once it lands in the virtual account. Discriminated union on `type` with two variants: - **`HOLD`** — dollars in, dollars held. The USD you send settles as a USD balance in the virtual account. - **`AUTO_CONVERT_AND_WITHDRAW`** — the incoming USD is converted into the selected `asset` and sent to the `external_account_id` previously linked to the participant. Required at creation; there is no implicit default. May be `null` in responses for accounts created before settlement policies were introduced.
    - object: Dollars in, dollars held — the USD you send settles as a USD balance in the virtual account.
      - `type` 'HOLD', required
    - object: Incoming USD is automatically converted into the selected `asset` and sent to the `external_account_id` previously linked to the participant. Both fields are required.
      - `type` 'AUTO_CONVERT_AND_WITHDRAW', required
      - `asset` string, required: Crypto asset the USD balance is converted into before withdrawal.
      - `external_account_id` string, uuid, required: External crypto account the converted funds are sent to. Must already be linked to the participant.
  - `provider_type` 'CRB': Banking partner to back the account. Optional; defaults to `CRB`, the only supported value. Any other value is rejected with `400`.
  - `virtual_account_name` string: Optional label for the account, used verbatim as the title on the banking partner’s own record. Omit it and the title is derived from the participant’s name as before. Set once at creation: a replay of the same `Idempotency-Key` returns the originally-stored name and does not update it. Must be 1-255 characters, each printable ASCII (U+0020-U+007E) or tab, and not only whitespace; anything else is rejected with 400.
  - `named_account` boolean: Optional; defaults to `false`. Persisted and echoed back, but drives no behaviour today — reserved for a future named-account feature.
  - `account_label` string: Optional. Routes deposits and AUTO_CONVERT_AND_WITHDRAW payouts to a named account instead of `general`. No leading/trailing special chars; max 40 chars. Invalid value returns `400`. Immutable.

## Response `200`

The `Idempotency-Key` was already used; the existing record is returned unchanged.

- VirtualAccount
  - `id` string, uuid: The virtual account identifier, equal to the `Idempotency-Key` supplied on creation.
  - `participant_code` string
  - `platform_code` string
  - `provider_type` 'CRB', nullable: Banking partner backing the account.
  - `status` 'PENDING' | 'ACTIVE' | 'CLOSING' | 'CLOSED' | 'BLOCKED' | 'FAILED', nullable: Lifecycle state. `PENDING` until the banking partner issues the account; `FAILED` when the banking partner declined to onboard the customer; `CLOSED`, `BLOCKED` and `FAILED` are terminal.
  - `currency` 'USD'
  - `virtual_account_name` string, nullable: Platform-supplied label for the account, set at creation and immutable afterwards. When supplied it is used verbatim as the title on the banking partner’s own record of the account. `null` when it was not supplied.
  - `named_account` boolean: Echoed back exactly as supplied at creation. Reserved for a future named-account feature; it drives no behaviour today, so do not branch on it.
  - `account_label` string: Label of the account deposits into this virtual account are credited to, set at creation and immutable afterwards. `general` when none was supplied.
  - `bank_details` BankDetails, nullable: Deposit instructions for the virtual account. Present only when `status` is `ACTIVE`, `CLOSING`, `CLOSED` or `BLOCKED`; `null` while the account is `PENDING`, when the banking partner has not yet issued the account, and always for `FAILED`.
    - `bank_account_number` string
    - `bank_routing_number` string
    - `provider_account_id` string: The banking partner's own identifier for the account.
    - `bank_name` string
  - `status_detail` StatusDetail, nullable: Supplementary context for a terminal failure. Populated when `status` is `FAILED` or `BLOCKED`; `null` otherwise. `code` is drawn from a closed, additive set — treat any value not listed in `enum` as a terminal, unclassified failure.
    - `code` 'VA_CREATION_DECLINED' | 'VA_CREATION_FAILED' | 'VA_BLOCKED'
    - `message` string
    - `at` string, date-time, nullable
  - `settlement_policy` union: Determines what happens to money once it lands in the virtual account. Discriminated union on `type` with two variants: - **`HOLD`** — dollars in, dollars held. The USD you send settles as a USD balance in the virtual account. - **`AUTO_CONVERT_AND_WITHDRAW`** — the incoming USD is converted into the selected `asset` and sent to the `external_account_id` previously linked to the participant. Required at creation; there is no implicit default. May be `null` in responses for accounts created before settlement policies were introduced.
    - object: Dollars in, dollars held — the USD you send settles as a USD balance in the virtual account.
      - `type` 'HOLD', required
    - object: Incoming USD is automatically converted into the selected `asset` and sent to the `external_account_id` previously linked to the participant. Both fields are required.
      - `type` 'AUTO_CONVERT_AND_WITHDRAW', required
      - `asset` string, required: Crypto asset the USD balance is converted into before withdrawal.
      - `external_account_id` string, uuid, required: External crypto account the converted funds are sent to. Must already be linked to the participant.
  - `created_at` string, date-time, nullable
  - `updated_at` string, date-time, nullable

## Other responses

- `202`: Accepted. The virtual account was created and is `PENDING` issuance by the banking partner.
- `400`: Bad Request
- `403`: Forbidden
- `404`: Not Found
- `409`: The participant cannot be onboarded to the banking partner, for example because the underlying customer record is in a terminal state.
- `500`: Internal Server Error
- `503`: Service Unavailable

## Changes

- **2026-11-03** `47cf70ae701c`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/zerohash/apis/zerohash-api/changes/virtual_accounts/post.md)

---

[API](https://skmtc.dev/zerohash/apis/zerohash-api.md) - [All operations](https://skmtc.dev/zerohash/apis/zerohash-api/llms.txt) - [OpenAPI document](https://skmtc.dev/zerohash/apis/zerohash-api/revisions/47cf70ae701c?raw)
