Virtual Account Updates

Overview

Virtual accounts emit one of two webhook payload types depending on what's changing. Changes to the account resource itself emit x-zh-hook-payload-type header value of virtual_account.status_changed, while activity on deposits landing in that account emit virtual_account_deposit.status_changed. Both events track their respective resource as it moves through its lifecycle - the account through creation, closure, blocking, and suspension; the deposit through receipt, screening, and payouts - but each uses its own set of status values reflecting its distinct pipeline. The table below consolidates the possible statuses across both event types and which type each status applies to.

Virtual Account Events

Subscribe to this event by setting the x-zh-hook-payload-type header value to virtual_account.status_changed.


A virtual_account.status_changed webhook is emitted every time a virtual account transitions between states - for example, when the account is first opened and becomes active, when zerohash blocks it, or when it's closed or suspended.

Virtual Account Statuses

The status field communicates the current state of the virtual account. Values are returned in lowercase.

StatusDescription
activeVirtual account has been successfully created and can receive deposits.
closedVirtual account has been closed. Terminal.
blockedzerohash has blocked the account; it can no longer receive deposits. Terminal.
failedzerohash could not open or had to reject the account. Terminal.

Note:

REST API responses (e.g. GET /virtual_accounts/{id}) use uppercase status values (PENDING, ACTIVE, CLOSED, BLOCKED, FAILED). This webhook uses lowercase values for the same states. The two are not interchangeable strings.

Virtual Account Payload Schema

FieldTypeDescription
event_typestringAlways virtual_account.status_changed.
virtual_account_idstringUnique identifier for the virtual account.
participant_codestringParticipant code of the end customer that owns the account.
platform_codestringPlatform the account belongs to.
provider_typestringBanking provider backing the account. Currently only CRB.
statusstringCurrent account status. See Virtual Account Statuses for possible values.
bank_detailsobjectThe account's bank details. Present only when status is active.
bank_details.account_numberstringThe account's account number.
bank_details.routing_numberstringThe account's routing number.
bank_details.provider_account_idstringThe banking provider's internal identifier for the account.
status_detailobjectAdditional context on why the account entered its current status. Present only for blockedand failed (and suspended which is not yet live, but coming soon).
status_detail.codestringStable, machine-readable reason code.
status_detail.messageHuman-readable description of the code. Do not parse, may be reworded.
status_detail.atstring (ISO 8601)Timestamp the status detail was recorded.
created_attimestampTimestamp the account was created.
updated_attimestampTimestamp the account was last updated.

Note:

event_type is specific to this webhook. The virtual_account_deposit.status_changed webhook does not include an event_type field, its type field is a resource type indicator (always DEPOSIT), not an event name.

Recovering from a failed Virtual Account

The status_detail.code on a failed event determines whether the participant can try again:

  • VA_CREATION_DECLINED: terminal for the participant. Any further POST /virtual_accounts for that participant, or for a different participant sharing the same tax ID, is rejected with 409 CUSTOMER_TERMINAL.
  • VA_CREATION_FAILED: terminal only for that specific account. A new POST /virtual_accounts for the same participant, using a new Idempotency-Key, creates a fresh account normally.

virtual_account.status_changed Payload Examples

Active

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "provider_type": "CRB",
  "status": "active",
  "bank_details": {
    "account_number": "1234567890",
    "routing_number": "021000021",
    "provider_account_id": "crb_ac_9f13e2"
  },
  "created_at": "2026-08-20T14:32:01Z",
  "updated_at": "2026-08-20T14:32:01Z"
}

Closed

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "provider_type": "CRB",
  "status": "closed",
  "created_at": "2026-08-20T14:32:01Z",
  "updated_at": "2026-08-25T09:10:44Z"
}

Blocked

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "provider_type": "CRB",
  "status": "blocked",
  "status_detail": {
    "code": "VA_BLOCKED",
    "message": "zerohash has blocked this virtual account and it can no longer receive deposits. Contact zerohash support for next steps.",
    "at": "2026-08-25T09:10:44Z"
  },
  "created_at": "2026-08-20T14:32:01Z",
  "updated_at": "2026-08-25T09:10:44Z"
}

Failed - Creation declined

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "provider_type": "CRB",
  "status": "failed",
  "status_detail": {
    "code": "VA_CREATION_DECLINED",
    "message": "zerohash could not open this virtual account for the account holder. Contact zerohash support for next steps.",
    "at": "2026-08-20T14:32:01Z"
  },
  "created_at": "2026-08-20T14:32:01Z",
  "updated_at": "2026-08-20T14:32:01Z"
}

Failed - Creation failed

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "provider_type": "CRB",
  "status": "failed",
  "status_detail": {
    "code": "VA_CREATION_FAILED",
    "message": "zerohash was unable to open this virtual account. Contact zerohash support to request a new account.",
    "at": "2026-08-20T14:32:01Z"
  },
  "created_at": "2026-08-20T14:32:01Z",
  "updated_at": "2026-08-20T14:32:01Z"
}

Virtual Account Deposit Events

Subscribe to this event by setting the x-zh-hook-payload-type header value to virtual_account_deposit.status_changed.
A virtual_account_deposit.status_changed webhook is emitted every time a deposit into a virtual account transitions between states - for example, when funds are received, when a deposit is held for review, or when a payout to the configured external account or wallet completes.

Virtual Account Deposit Statuses

The status field communicates the current stage of the deposit.

StatusDescription
deposit_receivedFunds landed at partner bank, but have not been credit yet.
deposit_pendingDeposit is pending further processing or review before it can be cleared.
deposit_clearedReview cleared and the deposit was credited to the participant.
deposit_rejectedDeposit was rejected and will be returned to the originator. Terminal.
deposit_blockedConfirmed sanctions match; funds quarantined. Terminal.
payout_initiatedConversion and payout to the configured external account or wallet has begun (only for AUTO_CONVERT_AND_WITHDRAW policy).
payout_completedConversion and payout completed successfully (only for AUTO_CONVERT_AND_WITHDRAW policy). Terminal.
payout_failedThe conversion or payout leg failed (only for AUTO_CONVERT_AND_WITHDRAW policy). Terminal.

Virtual Account Deposit Payload Schema

FieldTypeDescription
typestringAlways DEPOSIT.
statusstringCurrent deposit status. See Virtual Account Deposit Statuses (table above) for possible values.
transaction_idstringUnique identifier for this deposit. Matches deposit_reference_id on the corresponding account_balance.changed movement.
virtual_account_idstringThe virtual account the deposit landed in.
participant_codestringEnd customer's participant code.
platform_codestringPlatform the account belongs to.
amountstringDeposit amount. Absent on payout_* statuses.
currencystringDeposit currency. Absent on payout_* statuses.
railstringFunding rail, e.g. wire, ach, rtp.
(Note: rtp is inclusive of FedNow)
referencestringPlatform-supplied reference, if provided.
reasonstringSingle reason code. Present on deposit_pending, deposit_rejected, and deposit_blocked.
reasonsstring[]Every hold cause, when more than one applies. Present only on some deposit_pending events; reason is still set alongside it.
originator_namestringName on the originating account.
settlement_policy_typestringPresent only on payout_initiated.
external_account_idstringPayout destination account id. Present on all payout_* statuses.
assetstringPayout asset (e.g. USDC.SOL). Present on all payout_* statuses.
quantitystringSettled payout amount in asset units. Present only on payout_completed.
created_attimestampTimestamp of this event. On payout_completed specifically, this is the settlement time, not the deposit's original creation time.

virtual_account_deposit.status_changed Payload Examples

Deposit Received

Fires when a deposit lands at bank partner and is pending screening before being credited to customer.

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "transaction_id": "vat_3d9f6b2c1a8e4f7d",
  "type": "DEPOSIT",
  "status": "deposit_received",
  "amount": "2500.00",
  "currency": "USD",
  "originator_name": "Leslie Barbara Knope",
  "rail": "wire",
  "reference": "INV-10432",
  "created_at": "2026-08-25T09:10:44Z"
}

Note:

For Virtual Accounts with the HOLD settlement policy, subscribe to account_balance.changed to see when USD deposits are credited to customer balances. To match a deposit event to its balance movement:

  • transaction_id on this event equals deposit_reference_id on the balance movement
  • account_group is your platform code, account_label is the virtual account's label, account_type is available
  • run_type and movement_type are both deposit

See examples on Account Balance.

Deposit Pending

Fires when an incoming deposit is being held for manual review. This can happen because of a name mismatch on a wire, or if a deposit is above configured limits. reason is over_threshold | name_mismatch | other. When more than one cause applies, reasons lists all of them.

{
  "type": "DEPOSIT",
  "status": "deposit_pending",
  "transaction_id": "a790d94a-b6f0-42c0-9bfd-7065e4c7b7e9",
  "virtual_account_id": "b6778167-7a81-49c4-b5ae-e81e701173a9",
  "participant_code": "PART001",
  "platform_code": "PLAT001",
  "amount": "500.00",
  "currency": "USD",
  "rail": "wire",
  "reference": "INV-2026-00417",
  "reason": "over_threshold",
  "reasons": ["over_threshold"],
  "originator_name": "Acme Corp Treasury",
  "created_at": "2026-09-14T15:06:12Z"
}

Deposit Cleared

Fires when an incoming deposit has been approved during screening and will be credited to the end customer.

{
  "type": "DEPOSIT",
  "status": "deposit_cleared",
  "transaction_id": "a790d94a-b6f0-42c0-9bfd-7065e4c7b7e9",
  "virtual_account_id": "b6778167-7a81-49c4-b5ae-e81e701173a9",
  "participant_code": "PART001",
  "platform_code": "PLAT001",
  "amount": "500.00",
  "currency": "USD",
  "rail": "wire",
  "reference": "INV-2026-00417",
  "originator_name": "Acme Corp Treasury",
  "created_at": "2026-09-14T15:13:02Z"
}

Deposit Rejected

Fires when an incoming deposit will not be credited to the end customer, but funds are designated for return to the originator. Where "reason" can be = JURISDICTION_DENIED | NAME_MISMATCH

{
  "type": "DEPOSIT",
  "status": "deposit_rejected",
  "transaction_id": "a790d94a-b6f0-42c0-9bfd-7065e4c7b7e9",
  "virtual_account_id": "b6778167-7a81-49c4-b5ae-e81e701173a9",
  "participant_code": "PART001",
  "platform_code": "PLAT001",
  "amount": "500.00",
  "currency": "USD",
  "rail": "wire",
  "reference": "INV-2026-00417",
  "reason": "JURISDICTION_DENIED",
  "originator_name": "Acme Corp Treasury",
  "created_at": "2026-09-14T15:10:47Z"
}

Deposit Blocked

Fires when an incoming deposit will not be credited to the end customer, and funds will NOT be returned to originator due to sanctions screening results.

{
  "type": "DEPOSIT",
  "status": "deposit_blocked",
  "transaction_id": "a790d94a-b6f0-42c0-9bfd-7065e4c7b7e9",
  "virtual_account_id": "b6778167-7a81-49c4-b5ae-e81e701173a9",
  "participant_code": "PART001",
  "platform_code": "PLAT001",
  "amount": "500.00",
  "currency": "USD",
  "rail": "wire",
  "reference": "INV-2026-00417",
  "reason": "SANCTIONS_MATCH",
  "originator_name": "Acme Corp Treasury",
  "created_at": "2026-09-14T15:12:30Z"
}

Payout Initiated

Fires when an incoming deposit begins automatic conversion and payout to the account's configured external account. There is still a preceding deposit_received event for this settlement policy. This only applies to accounts with the settlement policy AUTO_CONVERT_AND_WITHDRAW.

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "transaction_id": "vat_9c4e1f7a2b6d3e8f",
  "type": "DEPOSIT",
  "originator_name": "Leslie Barbara Knope",
  "rail": "wire",
  "status": "payout_initiated",
  "external_account_id": "107e8a2a-c835-4b76-b49d-a633d45727b9",
  "asset": "USDC.SOL",
  "reference": "INV-10432",
  "created_at": "2026-08-25T09:10:44Z"
}

Payout Completed

Fires once the conversion and payout have completed successfully. This only applies to accounts with the settlement policy AUTO_CONVERT_AND_WITHDRAW. created_at on this event is the settlement timestamp, not the deposit's original creation time.

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "transaction_id": "vat_9c4e1f7a2b6d3e8f",
  "type": "DEPOSIT",
  "status": "payout_completed",
  "external_account_id": "107e8a2a-c835-4b76-b49d-a633d45727b9",
  "asset": "USDC.SOL",
  "quantity": "2499.7",
  "originator_name": "Leslie Barbara Knope",
  "rail": "wire",
  "reference": "INV-10432",
  "created_at": "2026-08-25T09:12:03Z"
}

Payout Failed

Fires if the conversion or payout leg fails. This only applies to accounts with the settlement policy AUTO_CONVERT_AND_WITHDRAW.

{
  "virtual_account_id": "47d7dfe4-fcae-45fe-9242-7e14795f449c",
  "participant_code": "CUST01",
  "platform_code": "PLAT01",
  "transaction_id": "vat_9c4e1f7a2b6d3e8f",
  "type": "DEPOSIT",
  "originator_name": "Leslie Barbara Knope",
  "rail": "wire",
  "status": "payout_failed",
  "external_account_id": "107e8a2a-c835-4b76-b49d-a633d45727b9",
  "asset": "USDC.SOL",
  "reference": "INV-10432",
  "created_at": "2026-08-25T09:10:44Z"
}