> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kirafin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# State machines

> The virtual account, deposit, and payout state machines — status vocabulary, transitions, and how casing differs across surfaces and API versions.

This is the authoritative reference for Kira's three resource state machines (virtual account, deposit, payout): the status vocabulary, the transitions, and how casing differs across surfaces. Use it to map status values to behavior.

For webhook delivery and signing, the full event catalog, and the envelope shapes, see [Webhooks](/guides/webhooks).

<Note>
  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](/guides/versioning) for how to pin.
</Note>

## Virtual account state machine

The virtual account `status` 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`.

```
approved (activating ──▶ active) ──▶ deactivated
   │
   └─(on provisioning failure)──▶ declined
```

| Wire value (`status`) | Meaning                                                                               |
| --------------------- | ------------------------------------------------------------------------------------- |
| `approved`            | Provisioned — covers BOTH activating AND active (does NOT by itself mean funds-ready) |
| `declined`            | Provisioning failed (terminal)                                                        |
| `deactivated`         | VA closed / disabled (terminal)                                                       |

<Note>
  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](/guides/rfis) instead — an account-level information request never was, and still isn't, surfaced as a virtual-account `status` value.
</Note>

**On the default / newer API versions** the raw enum is returned instead: `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).

<Warning>
  **`approved` ≠ funds-ready.** Because `approved` collapses *activating* and *active*, the API returns `status: "approved"` before funds can move. The `pending / activating / active` ladder is the raw-enum default, not what the `2026-04-14` pin returns.

  **Detect a truly-active VA** via `account_number` being a real account number — non-null AND `!= "PENDING-ACT-ACCOUNT"` (the sentinel is ACT-only; international / crypto VAs show `account_number: null` until provisioned) — or by handling the **`virtual_account.activated`** webhook (the only "funds-ready" event). On an active ACT VA, `GET /v1/virtual-accounts/{id}/balance` returns **`200`** with `available_balance` (while activating it returns `400`).
</Warning>

### Webhooks vs poll-only

* `virtual_account.created` fires when the VA is provisioned (may arrive while still activating).
* `virtual_account.activated` fires when it reaches active — the ONLY funds-ready webhook; clients must handle it.
* `failed` / `declined` and `deactivated` are **poll-only** — there is **no** `virtual_account.failed` and **no** `virtual_account.deactivated` event (and no client-facing deactivate/delete endpoint). Observe them via `GET /v1/virtual-accounts/{id}`.

## Deposit state machine

The coarse inbound deposit `status` is UPPERCASE. It applies to `GET /v1/virtual-accounts/{id}/deposits[/{depositId}]`.

```
PENDING ──▶ COMPLETED ──▶ REFUNDED     (bank claw-back / return AFTER credit)
   │
   ├──▶ FAILED                          (terminal)
   └──▶ REFUNDED                        (returned to sender, terminal)
```

| Wire value (`status`) | Meaning                                                                                   |
| --------------------- | ----------------------------------------------------------------------------------------- |
| `PENDING`             | Inbound funds detected, not yet credited                                                  |
| `COMPLETED`           | Credited — **not permanently final**: a bank claw-back / return can move it to `REFUNDED` |
| `FAILED`              | Inbound / settlement failed (terminal)                                                    |
| `REFUNDED`            | Returned to sender (terminal)                                                             |

There is **no separate `RETURNED` deposit status** — a return and a refund both collapse to `REFUNDED`.

<Warning>
  **Casing:** the GET resource `status` is **UPPERCASE** (`COMPLETED`), while flat deposit webhook events carry **lowercase** `data.status` (`completed`) — but casing is inconsistent across surfaces generally (`user.*` events carry UPPERCASE `data.status`), so ALWAYS compare case-insensitively.
</Warning>

**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.**

<Note>
  **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.**
</Note>

## Payout state machine

The payout `status` 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.

```
CREATED ──▶ PENDING ──▶ PROCESSING ──▶ COMPLETED        (happy path)
                          │
holds (resume after):     ├──▶ KYT_PENDING (compliance / txn-monitoring)
                          └──▶ IN_REVIEW   (manual review)
terminal: COMPLETED · FAILED · EXPIRED (crypto-only)
```

| Wire value (`status`) | Meaning                                                                |
| --------------------- | ---------------------------------------------------------------------- |
| `CREATED`             | Payout accepted, not yet queued                                        |
| `PENDING`             | Queued                                                                 |
| `PROCESSING`          | Sent to the rail / provider                                            |
| `COMPLETED`           | Delivered (terminal, success)                                          |
| `KYT_PENDING`         | Held for transaction-monitoring screening (resumes)                    |
| `IN_REVIEW`           | Held for manual review (resumes)                                       |
| `FAILED`              | Rejected / returned / cancelled (terminal)                             |
| `EXPIRED`             | Crypto payout never funded by its deadline (terminal, **crypto-only**) |

* **A beneficiary-bank RETURN** arrives as the `payout.returned` event carrying **`data.status: "returned"`** (lowercase). The payout **resource** then resolves to a terminal **`FAILED`** state on `GET /v1/payouts/{id}` and carries **`error_code: "va-payout-bank-returned"`** — note the `error_code` lives on the resource, **NOT** in the webhook payload. Returned funds come back to your balance (minus any return fee) to re-attempt. So `returned` is a valid `payout.*` event and `data.status` value, but it is **not** a `GET` resource status; there is no `DISPUTED` status at all.
* A provider **cancellation** arrives (on the ACT rail) as a **`payout.failed`** event carrying **`data.status: "cancelled"`**; the resource resolves to **`FAILED`** with a provider-cancelled `error_code`. `CANCELLED` is not a `GET` resource status, but the `payout.failed` event payload can carry `data.status: "cancelled"`.
* `EXPIRED` is 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_REVIEW` hold surfaces via `payout.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 with `GET /v1/payouts/{id}`. The same applies to a deposit held for screening (reconcile via `GET /v1/virtual-accounts/{id}/deposits/{depositId}`).

<Warning>
  **Casing is inconsistent across surfaces — ALWAYS compare statuses case-insensitively.** `GET` resource `status` is UPPERCASE (payouts, deposits; VAs follow the version vocabulary — lowercase `approved` on this pin). The payout `201` create response is lowercase `"created"` while `GET` returns `CREATED` for the same payout. Flat payout/VA webhook events carry lowercase `data.status` (`created`, `pending`, `processing`, `activating`); `payout.status_changed` carries UPPERCASE at `data.data.status`; `user.*` events carry UPPERCASE `data.status` (`CREATED`).
</Warning>

## Webhook events & delivery

Webhook **delivery and signing**, the full **event catalog** (which event fires on each transition, with the resulting status), and the **envelope shapes** all live in the [Webhooks](/guides/webhooks) guide. Each event there maps to the status values defined in the state machines above.
