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 intoexternal_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:
ACTIVEwhen the account is createdBLOCKEDorFAILEDif it can't proceed (each with astatus_detailexplaining why)CLOSEDwhen the account is closed. Use the status field for branching logic andstatus_detail.code(not the human-readable message) for any failure/block handling.
1. Opening a Hold Virtual Account
Example request (for an individual) - POST /virtual_accounts
{
"participant_code": "CUST01",
"settlement_policy": { "type": "HOLD" }
}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": "HOLD" },
"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": "HOLD" },
"created_at": "2026-08-24T14:22:00.000Z",
"updated_at": "2026-08-24T14:35:12.000Z"
}2. 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:
ACTIVEwhen the account is createdBLOCKEDorFAILEDif it can't proceed (each with astatus_detailexplaining why)CLOSEDwhen the account is closed. Use the status field for branching logic andstatus_detail.code(not the human-readable message) for any failure/block handling.
3. How to use USD balance after deposit
After USD is credited to your customers balance, you can leverage other endpoints to facilitate a crypto buy, stablecoin conversion, or fiat withdrawal. For example:
- To buy crypto, leverage POST /liquidity/rfq to get a quote and POST /liquidity/execute to execute quote
- For a stablecoin conversion and payout (or a fiat withdrawal), leverage POST /payments/external_accounts to link an external wallet address (or bank account) for your customer, and leverage POST /payments for a stablecoin (or fiat) withdrawal.
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_changedaccount_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:
deposit_received: The deposit has arrived at our partner bank. It has not been credited to the customer yet.deposit_pending(only if review is needed): The deposit is held for manual review. The reason field says why.deposit_cleared: The deposit passed review and will be credited to the customer.payout_initiated: zerohash has started converting the USD into the configured asset and sending it to the configuredexternal_account_id. The platform doesn’t need to do anything.- 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.
For Virtual Accounts with the HOLD settlement policy, each deposit sends a virtual_account_deposit.status_changed webhook every time its status changes, in this order:
deposit_received: The deposit has arrived at our partner bank. It has not been credited to the customer yet.deposit_pending(only if review is needed): The deposit is held for manual review. The reason field says why.deposit_cleared: The deposit passed review and will be credited to the customer.
When the USD is credited to the customer's balance, account_balance.changed fires. Treat this as the signal that the funds can be used for a buy, conversion or withdrawal.
A deposit can also end as deposit_rejected (funds are returned to the sender) or deposit_blocked (funds are quarantined and not returned). Payout statuses are never sent for HOLD accounts.
Updated 5 days ago