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

# Test the full lifecycle in the sandbox

> End-to-end sandbox walkthrough: create and verify a user, open a virtual account and watch it auto-activate, simulate a deposit, price a quotation, pay out, and watch each step complete.

This walkthrough drives one virtual account through its whole life against the sandbox: create and verify a user, open the virtual account and watch it **activate on its own**, **simulate a deposit** to exercise your notification handling, **price a quotation**, create an **outbound payout**, and watch the deposit and payout **complete**. It stitches together the [Quickstart](/quickstart), [Creating users](/guides/creating-users), [Itemized quotations](/guides/itemized-quotations), and [Payouts, quotations, and recipients](/guides/payouts-quotations-recipients) guides into a single testing script.

Every example uses the sandbox base URL `https://api.balampay.com/sandbox`; in production, drop the `/sandbox` prefix. Every authenticated call carries both `x-api-key` and `Authorization: Bearer <access token>` (see [Authentication](/guides/authentication)).

<Info>
  **How the sandbox behaves — read this first.** Three sandbox behaviors shape every test below:

  * **Verification is automatic.** A user that is complete for a product verifies on its own — usually within seconds — with no verification call for you to make. It can instead route to manual review (`status: REVIEW`).
  * **Virtual accounts activate automatically.** The sandbox polls provider status every 15 seconds, so a new VA becomes funds-ready in about a minute and fires `virtual_account.activated` on its own.
  * **A simulated deposit credits the VA balance.** On the fiat wire rail, `simulate-deposit` fires the deposit **event and webhook** *and* credits the virtual account's available balance; a payout then **debits** that same balance, so `deposit → balance → payout` is one connected cycle (`GET /v1/virtual-accounts/{id}/balance` reflects it, possibly after a few seconds). A VA may also open with a provider-side sandbox float, so a payout can succeed before you simulate a deposit — always confirm the balance covers the amount first. Balance behavior is not uniform across every rail, so treat the `virtual_account.deposit_funds_received` webhook as the rail-independent signal. See [Known limitations](/guides/known-limitations#deposits).
</Info>

<Steps>
  <Step title="Authenticate and pin your version">
    Exchange your credentials for a bearer token, then send `X-Api-Version: 2026-04-14` on every request. See the [Quickstart](/quickstart) for the full credential setup.

    ```bash theme={null}
    export KIRA_API_KEY="<your api_key>"
    export ACCESS_TOKEN="<data.access_token from POST /auth>"
    ```

    The rest of this guide assumes both are exported. Each `curl` below repeats the two credential headers and the version header.
  </Step>

  <Step title="Create and verify a user">
    Create the user with `POST /v1/users`. Send the **product-required compliance fields at creation** — including `expected_monthly_volume` and `expected_transaction_count` — so they are part of the initial verification. Supplying them later, after the user is `VERIFIED`, re-triggers verification and can temporarily block virtual-account creation (see [Known limitations](/guides/known-limitations#identity--user-lifecycle)).

    ```bash theme={null}
    curl https://api.balampay.com/sandbox/v1/users \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "type": "individual",
        "first_name": "Ana",
        "last_name": "García",
        "email": "ana+sandbox@example.com",
        "source_of_funds": "salary",
        "expected_monthly_volume": "10000_to_49999",
        "expected_transaction_count": "11_to_50"
      }'
    ```

    This minimal body returns `201`, but a user only verifies once it is **complete** for a product — send the full field set for its category (documents, address, questionnaire) as described in [Creating users](/guides/creating-users). Then poll until the user is verified, gating on `status` (not `verification_status`, which can contradict it):

    <Warning>
      **`VERIFIED` is necessary but not sufficient — the next step re-checks product eligibility.** `POST /v1/virtual-accounts` evaluates the user against the `US_BANK` product's required fields, which go beyond the two above: identity + documents, `account_purpose`, `employment_status` and its follow-ups (`current_employer` / `occupation` / `income_source`), `ssn` (required for US citizens), and the US-bank questions (`has_us_bank_account`, `has_denied_bank_account`). Send them all at user creation. If any are missing the create call still returns `201` and the VA is created, but it **stalls in `activating`** instead of activating — see the troubleshooting note in the next step.
    </Warning>

    ```bash theme={null}
    export USER_ID="<data.id from the create response>"

    curl https://api.balampay.com/sandbox/v1/users/$USER_ID \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14"
    ```

    <Warning>
      **Gate on `status: VERIFIED`.** Document files must be real, retrievable bytes — verification downloads each `file` at submission, so placeholder or unreachable document URLs leave the user stuck in `CREATED` even though `verification_triggered` was `true`. A terminal `REJECTED` means the provider declined the data; the reason arrives only in the `user.verification.failed` webhook. See [State machines](/guides/state-machines).
    </Warning>
  </Step>

  <Step title="Open a virtual account and watch it activate">
    Create the virtual account for the verified user with `POST /v1/virtual-accounts`. In the sandbox use `bank: "slovak_savings_bank"` (in production the same rail is `bank: "portage"`).

    ```bash theme={null}
    curl https://api.balampay.com/sandbox/v1/virtual-accounts \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{ "user_id": "'"$USER_ID"'", "type": "US_BANK", "mode": "fiat", "bank": "slovak_savings_bank" }'
    ```

    ```bash theme={null}
    export VA_ID="<data.id from the create response>"
    ```

    The VA activates on its own. The sandbox polls provider status every 15 seconds, so within about a minute the `virtual_account.activated` webhook fires and the account becomes funds-ready. Confirm readiness by reading the VA until `account_number` is a real value — it stays `null` while an international / crypto VA provisions (on the ACT rail, `bank: "austin_capital_trust"`, it shows the `"PENDING-ACT-ACCOUNT"` placeholder instead).

    ```bash theme={null}
    curl https://api.balampay.com/sandbox/v1/virtual-accounts/$VA_ID \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14"
    ```

    <Note>
      `approved` does **not** mean funds-ready — confirm with `account_number` and the `virtual_account.*` webhooks. While the VA is still activating, `GET /v1/virtual-accounts/{id}/balance` returns `400`. If a VA stays in `activating` well beyond a minute, read its `status_reason`: a value like `User is not eligible for US_BANK: Missing required fields for this product [missing: …]` means the user was missing product-required fields at creation (see the previous step). The VA does **not** self-heal — complete those fields on the user, then create a **new** VA. If `status_reason` is empty and it still won't activate, contact your Kira representative. See [Known limitations](/guides/known-limitations#virtual-accounts).
    </Note>
  </Step>

  <Step title="Simulate a deposit">
    `POST /v1/virtual-accounts/{virtual_account_id}/simulate-deposit` has **two modes**, chosen by the request body: send `{ amount }` (below) to simulate an **inbound deposit** into an **active** VA, or `{ transferUuid }` to **fund a crypto-funded payout** (see the note at the end of this step). It is **sandbox-only** — the same call returns `403 "Simulate deposit is only available in sandbox environments"` in production. The amount mode exercises your deposit-notification, reconciliation, and webhook handling.

    ```bash theme={null}
    curl https://api.balampay.com/sandbox/v1/virtual-accounts/$VA_ID/simulate-deposit \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14" \
      -H "Content-Type: application/json" \
      -d '{ "amount": "100.00", "payment_type": "wire", "sender_name": "Acme Holdings LLC" }'
    ```

    * `amount` — a positive USD string with **up to 2 decimals** (more returns `400`).
    * `payment_type` — the inbound rail: `wire` (default) or `ach`.
    * `sender_name` — optional; defaults to `"Simulated Sender"`.

    A `201` returns the simulated deposit. `status` is **lowercased** and depends on the VA mode and amount:

    ```json theme={null}
    {
      "deposit_id": "…",
      "virtual_account_id": "…",
      "amount": "100.00",
      "currency": "USD",
      "status": "completed",
      "settlement_triggered": false,
      "fees": { "…": "…" },
      "net_amount": "100.00",
      "net_currency": "USD"
    }
    ```

    | Condition               | `status`    | Notes                                                                                |
    | ----------------------- | ----------- | ------------------------------------------------------------------------------------ |
    | `fiat` VA, any amount   | `completed` | `settlement_triggered: false` is **normal** — a fiat deposit has no settlement step. |
    | `crypto` VA, any amount | `pending`   | The settlement pipeline then advances it to `completed`.                             |
    | `amount` equals `11`    | `refunded`  | A preserved magic value for exercising the refunded branch; no settlement.           |

    The call also fires the `virtual_account.deposit_funds_received` webhook. The `fees` and `net_amount` in the response mirror what the real settlement path would charge, using **your configured fee schedule** — they are not fixed numbers.

    <Note>
      **A simulated deposit credits the VA balance.** A fiat deposit completes at simulation time (`status: completed`, `settlement_triggered: false` — there is **no** async settle step to wait for) and, on the fiat wire rail, credits the virtual account's available balance; a crypto deposit starts `pending` and the settlement pipeline advances it to `completed`. Confirm it either through the `virtual_account.deposit_funds_received` **webhook** (recommended for event-driven handling) **or** by the balance delta on `GET /v1/virtual-accounts/{id}/balance` (which may take a few seconds to reflect). It also appears in `GET /v1/virtual-accounts/{id}/deposits`. Because a payout **debits** that same balance, the deposit above and the payout below form one connected `deposit → balance → payout` cycle. A payout draws on the VA's **whole** available balance (any opening sandbox float plus the deposits you credit), and not every rail moves its balance on a simulated deposit — so top the balance up first and keep the webhook as the rail-independent signal. See [Known limitations](/guides/known-limitations#deposits).
    </Note>

    <Note>
      **Second mode — fund a crypto-funded payout with `{ transferUuid }`.** A **crypto-funded** payout (priced with `payment_instructions` — see [Crypto-funded off-ramp](/guides/itemized-quotations#crypto-funded-off-ramp-both-inbound-and-conversion)) is created `CREATED`, awaiting an on-chain deposit to its per-payout wallet. Instead of `{ amount }`, send the payout's transfer id as `{ "transferUuid": "…" }` to **simulate that funding deposit** so the payout can proceed — no real stablecoin transfer needed.

      ```bash theme={null}
      curl https://api.balampay.com/sandbox/v1/virtual-accounts/$VA_ID/simulate-deposit \
        -H "Authorization: Bearer $ACCESS_TOKEN" \
        -H "x-api-key: $KIRA_API_KEY" \
        -H "X-Api-Version: 2026-04-14" \
        -H "Content-Type: application/json" \
        -d '{ "transferUuid": "<payout transfer id>" }'
      ```

      Send **exactly one** of `amount` or `transferUuid` — a body with both, or neither, returns `400`. Note `transferUuid` is **camelCase**, while the amount-mode fields are snake\_case. The `201` returns `{ "transfer_uuid", "status", "job_enqueued": true, "deposit_wallet": { "address", "network", "currency" } }`. The transfer must be a crypto-funded payout **attached to this VA** and **still awaiting funding** (`status: CREATED`) with a minted deposit wallet — otherwise `404 "Transfer not found"` (also returned for a foreign-client or wrong-VA transfer) or `400` (`"Transfer is not a crypto-funded payout"`, `"… missing deposit wallet"`, or `"Transfer is not awaiting funding (status: …)"`). Repeat calls are **idempotent** (a deterministic synthetic `txHash`), so re-sending won't double-fund.
    </Note>
  </Step>

  <Step title="Create a recipient for the payout">
    A payout's rail comes from the **recipient**, so create one with the `account_type` you want to pay over. A `slovak_savings_bank` VA supports `WIRE`.

    ```bash theme={null}
    curl https://api.balampay.com/sandbox/v1/recipients \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "user_id": "'"$USER_ID"'",
        "account": { "account_type": "WIRE", "…": "… bank details for the WIRE rail …" }
      }'
    ```

    ```bash theme={null}
    export RECIPIENT_ID="<recipient_id from the response>"
    ```

    <Note>
      The recipient response uses `recipient_id` (not `id`) and `created_ts` (not `created_at`), and the request `account` object comes back as a top-level `account_type` plus `account_details`. Recipients are **immutable** and are scoped to your client — a payout may target any recipient in your client. See [Payouts, quotations, and recipients](/guides/payouts-quotations-recipients) and [Known limitations](/guides/known-limitations#recipients).
    </Note>
  </Step>

  <Step title="Price a quotation">
    `POST /v1/quotations` prices the payout. Pass `virtual_account_id` to price against the VA and **persist a redeemable `quote_id`** that locks the fees and FX rate; add `from_held_balance: true` so the quote can be redeemed by a payout that debits the VA balance.

    ```bash theme={null}
    curl https://api.balampay.com/sandbox/v1/quotations \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-06-01" \
      -H "Content-Type: application/json" \
      -d '{
        "virtual_account_id": "'"$VA_ID"'",
        "amount": "50.00",
        "target": { "currency": "USD" },
        "from_held_balance": true
      }'
    ```

    ```bash theme={null}
    export QUOTE_ID="<quote_id from the response>"
    ```

    The response returns `source` and `recipient` money objects, a depeg-aware `conversion` block, an itemized `fees[]` array, and `totals`; the quote expires at `quote_expires_at` (default 15 minutes). The itemized line-by-line breakdown is returned on the `2026-06-01` pin used above — earlier versions return a simpler preview shape. For a **pre-account preview** that does not persist, send `quote_for: { rail, currency }` instead of `virtual_account_id` (it returns `quote_id: null`). See [Itemized quotations](/guides/itemized-quotations).

    <Note>
      Quote the flow with the **same rail** as the recipient you will pay — a quote pins the money (amounts, currencies, network), not the rail, and redeeming it against a different rail can apply a price that no longer matches. See [Payouts, quotations, and recipients](/guides/payouts-quotations-recipients#what-a-quote-enforces-at-redemption).
    </Note>
  </Step>

  <Step title="Create the outbound payout">
    First confirm the VA's balance can cover the source `amount`. The payout debits the VA's available sandbox balance — which reflects any simulated deposits you have credited plus any opening sandbox float:

    ```bash theme={null}
    curl https://api.balampay.com/sandbox/v1/virtual-accounts/$VA_ID/balance \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14"
    ```

    Preview to see the fee breakdown, then create the payout, redeeming the quote by passing `quote_id`:

    ```bash theme={null}
    # Preview — returns a fee breakdown
    curl https://api.balampay.com/sandbox/v1/virtual-accounts/$VA_ID/payout/preview \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14" \
      -H "Content-Type: application/json" \
      -d '{ "recipient_id": "'"$RECIPIENT_ID"'", "amount": "50.00" }'

    # Create the payout (redeeming the quote)
    curl https://api.balampay.com/sandbox/v1/virtual-accounts/$VA_ID/payout \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{ "recipient_id": "'"$RECIPIENT_ID"'", "amount": "50.00", "quote_id": "'"$QUOTE_ID"'" }'
    ```

    ```bash theme={null}
    export PAYOUT_ID="<payout id from the create response>"
    ```

    <Warning>
      If the available balance is **less** than the source `amount`, the payout is rejected synchronously with `400 "Insufficient balance. Available: … Required: …"`. That rejection is terminal — simulate the deposit **first** so the balance covers the amount; a later deposit raises the balance for the **next** payout but does not retroactively complete an already-rejected one. There is no fee-schedule endpoint and no `dry_run`; an amount below the minimum returns `400 "Total fees exceed or equal the payout amount"`, found only by iterating. The `201` create response returns lowercase `status: "created"`, while `GET /v1/payouts/{id}` returns `CREATED` for the same payout — compare statuses **case-insensitively**. See [Known limitations](/guides/known-limitations#payouts).
    </Warning>
  </Step>

  <Step title="Watch each step complete">
    Two things complete in this walkthrough. You observe each through **status plus webhook** — and, on the fiat wire rail, the deposit is also visible as a **balance delta** on `GET /v1/virtual-accounts/{id}/balance`.

    **The deposit** completes at simulation time: a fiat deposit returns `status: completed` immediately, credits the VA's available balance, and fires `virtual_account.deposit_funds_received`. Handle that webhook to reconcile the inbound notification.

    **The payout** advances through its state machine on its own in the sandbox. Poll `GET /v1/payouts/{id}` until it reaches a terminal status, or consume the webhooks:

    ```bash theme={null}
    curl https://api.balampay.com/sandbox/v1/payouts/$PAYOUT_ID \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "x-api-key: $KIRA_API_KEY" \
      -H "X-Api-Version: 2026-04-14"
    ```

    The status walks `CREATED` → `PENDING` → `PROCESSING` → `COMPLETED` (a KYT compliance screen can add a non-terminal `KYT_PENDING`; a terminal failure surfaces as `FAILED`). The matching webhooks are `payout.created`, `payout.pending`, `payout.processing`, and `payout.completed` (plus `payout.status_changed` on every transition). Completion is not guaranteed instant — treat it like VA activation and drive your flow off the webhook or a poll, not a fixed wait.

    <Note>
      **Casing differs across surfaces — compare case-insensitively.** `GET /v1/payouts/{id}` returns UPPERCASE `status`; the flat `payout.*` webhooks carry lowercase `data.status`; `payout.status_changed` carries UPPERCASE at `data.data.status`. See [State machines](/guides/state-machines) for the full vocabulary and [Webhooks](/guides/webhooks) for the event catalog and payload shapes.
    </Note>
  </Step>
</Steps>

## What completes, and how you observe it

| Step              | Completion signal                                                                                                                                | Do **not** rely on                                                                                                        |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| User verification | `status: VERIFIED` (poll `GET /v1/users/{id}`) + `user.status_changed`                                                                           | `verification_status` (can contradict `status`)                                                                           |
| VA activation     | `account_number` is a real value + `virtual_account.activated`                                                                                   | `approved` alone                                                                                                          |
| Simulated deposit | `status: completed` in the response + `virtual_account.deposit_funds_received` + the credited balance on `GET /v1/virtual-accounts/{id}/balance` | an instant *async* settle step — a fiat deposit is already `completed` (`settlement_triggered: false`)                    |
| Payout            | `GET /v1/payouts/{id}` → `COMPLETED` + `payout.completed`                                                                                        | an instant terminal status — completion is not guaranteed immediate, so drive off the webhook or a poll, not a fixed wait |

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/guides/webhooks">
    The event catalog and payload shapes for the user, virtual-account, deposit, and payout events used above.
  </Card>

  <Card title="State machines" icon="diagram-project" href="/guides/state-machines">
    The full status vocabulary and transitions, and how casing differs across surfaces and versions.
  </Card>

  <Card title="Itemized quotations" icon="receipt" href="/guides/itemized-quotations">
    The full pricing surface of `POST /v1/quotations` and how to lock and redeem a quote.
  </Card>

  <Card title="Known limitations" icon="triangle-exclamation" href="/guides/known-limitations">
    Every sandbox and API quirk referenced here, in one place.
  </Card>
</CardGroup>
