Create and Use Virtual Accounts

Create a Virtual Account linked to a customer for deposits and stablecoin or crypto conversions

Settlement Policies for Virtual Accounts - 2 types

The 2 settlement policies for Virtual Accounts are:

  • AUTO_CONVERT_AND_WITHDRAW: When a customer's deposit settles, the USD is automatically converted into a stablecoin/crypto asset and automatically sent on-chain to an external wallet.

  • HOLD: Customer's balance will be credited USD when the deposit settles. The customer can choose if/when to buy an asset or withdraw fiat to a linked bank account (separate integrations required).

The below API flows assume that you have already created an End Customer (see references to participant_code within the payloads below. The Onboarding instructions can be found here.)

Creating Virtual Accounts

1. Create a destination blockchain address

Once you have created an End Customer, you then need to link a destination blockchain address to that customer via the POST /payments/external_accounts endpoint.

Example request:

{
  "participant_code": "CUST01",
  "type": "crypto",
  "details": {
    "network": "SOL",
    "supported_assets": ["USDC"],
    "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
  }
}

Example response:

{
    "request_id": "a9e2c6fb-f738-4ecb-986c-befd70678707",
    "external_account_id": "107e8a2a-c835-4b76-b49d-a633d45727b9",
    "participant_code": "CUST01",
    "platform_code": "PLAT01",
    "account_nickname": "",
    "created_at": "2024-10-11T00:52:21.865Z",
    "status": "pending",
    "type": "crypto",
    "details": {
        "network": "SOL",
        "supported_assets": [
            "USDC"
        ],
        "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
        "destination_tag": ""
    }
}

The external account will initially enter a status of submitted. Immediately and automatically after an external account submission, zerohash will run the address through various checks. If the checks pass, the external account will enter a state of approved. Else, rejected.

2. Opening an auto-convert Virtual Account

Example request (for an individual) - POST /virtual_accounts

{
  "participant_code": "CUST01",
  "settlement_policy": {
    "type": "AUTO_CONVERT_AND_WITHDRAW",
    "asset": "USDC",
    "external_account_id": "7c1e2c3a-4b5d-4e6f-8a9b-0c1d2e3f4a5b"
  }
}

Note: When the platform selects AUTO_CONVERT_AND_WITHDRAW as the settlement policy, they must share the following fields in the account opening request:

  • asset = stablecoin/crypto asset that the customer's deposit will automatically be converted into
  • external_account_id = external wallet address where the stablecoin/crypto asset will be paid out to

Example response when status = PENDING

Note: status will display PENDING and bank_details will display null

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "provider_type": "CRB",
  "status": "PENDING",
  "currency": "USD",
  "bank_details": null,
  "status_detail": null,
  "settlement_policy": { "type": "AUTO_CONVERT_AND_WITHDRAW" },
  "created_at": "2026-08-24T14:22:00.000Z",
  "updated_at": "2026-08-24T14:22:00.000Z"
}

Example response when status = ACTIVE

Note: status will display ACTIVE and bank_details will display with customer's unique account number and routing number for the Virtual Account

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "provider_type": "CRB",
  "status": "ACTIVE",
  "currency": "USD",
  "bank_details": {
    "bank_account_number": "0000000001",
    "bank_routing_number": "123456789",
    "provider_account_id": "0000000001",
    "bank_name": "Cross River Bank"
  },
  "status_detail": null,
  "settlement_policy": { "type": "AUTO_CONVERT_AND_WITHDRAW" },
  "created_at": "2026-08-24T14:22:00.000Z",
  "updated_at": "2026-08-24T14:35:12.000Z"
}

3. Webhooks

After Virtual Account creation, the account moves through a status-based state machine PENDING → ACTIVE, or into a terminal BLOCKED, FAILED, or CLOSED state), and platforms receive a status change to the virtual_account.status_changed webhook on each transition:

  • ACTIVE when the account is created
  • BLOCKED or FAILED if it can't proceed (each with a status_detail explaining why)
  • CLOSED when the account is closed. Use the status field for branching logic and status_detail.code (not the human-readable message) for any failure/block handling.

Using the Virtual Accounts

Fiat Rails

Once a Virtual Account reaches ACTIVE status, the returned bank_details (account and routing number) function as a standard US bank account for receiving USD. The end customer can push a payment to the Virtual Account from their own bank using whichever rail they prefer (wire, RTP, or ACH) with no additional integration required on the Platform's side to receive these incoming push payments.

Webhooks

See dedicated webhook page here. You will need to subscribe to two webhooks:

  • virtual_account_deposit.status_changed
  • account_balance.changed

In summary:

For Virtual Accounts with the AUTO_CONVERT_AND_WITHDRAW settlement policy, each deposit sends a virtual_account_deposit.status_changed webhook every time its status changes, in this order:

  1. deposit_received: The deposit has arrived at our partner bank. It has not been credited to the customer yet.
  2. deposit_pending (only if review is needed): The deposit is held for manual review. The reason field says why.
  3. deposit_cleared: The deposit passed review and will be credited to the customer.
  4. payout_initiated: zerohash has started converting the USD into the configured asset and sending it to the configured external_account_id. The platform doesn’t need to do anything.
  5. One final status:
    • payout_completed: The conversion and payout succeeded.
    • payout_failed: The conversion or payout failed.

Before #4 occurs above, the account_balance.changed webhook will be sent to notify that the customer's balance has been credited, which automatically kicks off the conversion and payout.

Instead of a payout, a deposit can end as deposit_rejected (funds are returned to the sender) or deposit_blocked (when funds are quarantined and not returned). No payout events follow either of these.


Did this page help you?