All values here are code- and live-sandbox-verified. The vocabulary below assumes you pin
X-Api-Version: 2026-04-14; the raw-enum default is called out where it differs. See Versioning for how to pin.Virtual account state machine
The virtual accountstatus vocabulary is version-dependent — it is set by your pinned X-Api-Version.
On the 2026-04-14 pin (the version this doc tells you to pin to): approved → declined, plus deactivated.
This pin’s mapping is also code-defined to return
rfi for an “awaiting more info” hold on the account itself — but no virtual account reaches that state today; every VA that doesn’t provision resolves straight to declined. As of API version 2026-06-01, rfi names the RFI resource instead — an account-level information request never was, and still isn’t, surfaced as a virtual-account status value.pending → activating → active, plus failed, deactivated. Pin to 2026-04-14 to get the approved/declined vocabulary this doc uses.
Underlying transition path (same machine, two vocabularies): pending → activating → active/approved → deactivated; any provisioning step → failed/declined (terminal).
Webhooks vs poll-only
virtual_account.createdfires when the VA is provisioned (may arrive while still activating).virtual_account.activatedfires when it reaches active — the ONLY funds-ready webhook; clients must handle it.failed/declinedanddeactivatedare poll-only — there is novirtual_account.failedand novirtual_account.deactivatedevent (and no client-facing deactivate/delete endpoint). Observe them viaGET /v1/virtual-accounts/{id}.
Deposit state machine
The coarse inbound depositstatus is UPPERCASE. It applies to GET /v1/virtual-accounts/{id}/deposits[/{depositId}].
There is no separate
RETURNED deposit status — a return and a refund both collapse to REFUNDED.
Refund/return detection (rail-dependent): a returned incoming deposit ALWAYS → status: REFUNDED, delivered via ONE of two event names depending on the rail — deposit_funds_received re-sent with data.status: "refunded", OR deposit_returned. Branch on data.status == "refunded" (or resource status == REFUNDED), NOT on the event name.
A simulated deposit credits the sandbox balance. On the fiat wire rail a
simulate-deposit credits the VA’s available balance and a payout debits it, so deposit → balance → payout is a real cycle; GET /…/balance reflects the credit (possibly after a few seconds). The balance also includes any opening sandbox float, so a payout can also succeed without a prior deposit, and not every rail moves its balance on a simulated deposit — so treat the virtual_account.deposit_funds_received webhook as the rail-independent signal. The simulated deposit also appears in GET /v1/virtual-accounts/{id}/deposits (don’t rely on the list staying empty).A deposit’s settlement_triggered field is true only for crypto deposits ≥ $1; for fiat it is false because the fiat deposit is already completed (no separate settlement leg). settlement_triggered: false on a fiat deposit is normal — it is NOT an error.Payout state machine
The payoutstatus is UPPERCASE on GET (but NOT on every surface). It is UPPERCASE in GET /v1/payouts/{id}, in the list, and inside payout.status_changed’s data.data.status. BUT the 201 create response returns status: "created" lowercase (the same payout then reads CREATED on GET), and the flat payout.* webhook events carry lowercase data.status. Compare case-insensitively.
- A beneficiary-bank RETURN arrives as the
payout.returnedevent carryingdata.status: "returned"(lowercase). The payout resource then resolves to a terminalFAILEDstate onGET /v1/payouts/{id}and carrieserror_code: "va-payout-bank-returned"— note theerror_codelives on the resource, NOT in the webhook payload. Returned funds come back to your balance (minus any return fee) to re-attempt. Soreturnedis a validpayout.*event anddata.statusvalue, but it is not aGETresource status; there is noDISPUTEDstatus at all. - A provider cancellation arrives (on the ACT rail) as a
payout.failedevent carryingdata.status: "cancelled"; the resource resolves toFAILEDwith a provider-cancellederror_code.CANCELLEDis not aGETresource status, but thepayout.failedevent payload can carrydata.status: "cancelled". EXPIREDis crypto-only and is NOT a valid?status=list filter.- Crypto payouts add a settlement phase before the outbound leg (you submit the on-chain
txHash, Kira settles, then the fiat/outbound leg runs). - Compliance holds resolve on the resource. A
KYT_PENDING/IN_REVIEWhold surfaces viapayout.status_changed, but its resolution (cleared or rejected) is reflected on the resource and may not arrive as a separate terminal webhook — confirm the final status withGET /v1/payouts/{id}. The same applies to a deposit held for screening (reconcile viaGET /v1/virtual-accounts/{id}/deposits/{depositId}).