---
updatedAt: 2026-09-30T15:54:20.000Z
---

Fetch the complete documentation index at: https://docs.avvio.xyz/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Track status & failures

A payout is always in one of five statuses, and your ledger should model the
same five. Read it at `GET /payments/organizations/{orgId}/orders/{payoutId}`
or from its `payout.*` webhook, and accept any forward transition from the
status you hold.

| Status       | Meaning                                          | Your balance                                    | Terminal? |
| ------------ | ------------------------------------------------ | ----------------------------------------------- | --------- |
| `pending`    | Accepted, not yet sent to the payment network    | Held                                            | No        |
| `processing` | Handed to the payment network                    | Committed                                       | No        |
| `completed`  | The recipient's bank paid them                   | Sent                                            | Almost (see the bank return below) |
| `failed`     | Not paid, or returned. See `failureCode`         | Credited back as a separate entry when `fundsReturned` is `true` | Yes |
| `canceled`   | Stopped before sending; only possible while waiting on your own funding | Never left. `fundsReturned: true` | Yes |

## State transitions

A payout can make only these transitions:

```text
pending    -> processing | completed | failed | canceled
processing -> completed | failed
completed  -> failed        (returned by the recipient's bank)
```

Not every payout passes through every status. A rail that settles on acceptance
goes from `pending` straight to `completed`, and a status entered and left
between two reads is never reported. In the sandbox, every payout is
`processing` for about 8 seconds; suffix `0002` holds it for about 50.

### The bank return (`completed` → `failed`)

> [!WARNING]
> A receiving bank can return a payment days later, after an account freeze, a
> name mismatch or a compliance check. The payout moves from `completed` to
> `failed` with `failureCode: "returned_by_bank"` and `fundsReturned: true`. A
> ledger that treats `completed` as final shows it as paid, so keep reading
> `GET /events?since=` for settled payouts.

## The `fundsReturned` flag

On `failed` or `canceled`, `fundsReturned: true` means the money is back in your
balance (for `canceled`, it never left). Absent means it is not confirmed back,
so don't re-credit anyone yet. Read the flag, not the code: a
`compliance_rejected` payout has no `fundsReturned` while the funds are held for
review. In `GET /balance_transactions`, a `payout` debit with no matching
`payout_return` row means the money did not come back.

## Failure codes

A `failed` payout carries a `failureCode`. It is a payout state, not an error
response.

| `failureCode`                 | What happened                                   | Is the money back?    | What to do                                        |
| ----------------------------- | ----------------------------------------------- | --------------------- | ------------------------------------------------- |
| `returned_by_bank`            | It settled, then the receiving bank returned it | Yes (`fundsReturned: true`) | Tell your user. Reverse whatever you credited |
| `account_invalid`             | The account details are wrong                   | Check `fundsReturned` | Ask for correct details, create a new recipient   |
| `account_cannot_receive`      | The account cannot accept this payment          | Check `fundsReturned` | Try another account or corridor                   |
| `compliance_rejected`         | Refused by compliance screening                 | **Not automatically** | Contact us with the `payoutId`. Do not retry      |
| `limit_exceeded`              | Above a corridor or account limit               | Check `fundsReturned` | Split it, or check `limits` on the corridors call |
| `quote_expired`               | Too long between quoting and sending            | Check `fundsReturned` | Re-quote and send again                           |
| `authorization_not_completed` | An authorization step was not finished          | Check `fundsReturned` | Start again                                       |
| `execution_failed`            | It did not go through, cause not established    | Check `fundsReturned` | Retry only once the money is back (below)         |

"Check `fundsReturned`" means exactly that: on some routings these codes arrive
without the flag while the funds are still unconfirmed. The money is back only
when the payout carries `fundsReturned: true`, or when `GET /balance_transactions`
shows a `payout_return` row for it. Until then, a retry under a new idempotency
key debits your balance a second time while the first debit may still be held.

The enum also lists `insufficient_funds` and `unknown`, which no payout carries
today: a short balance is refused up front with `400 INSUFFICIENT_BALANCE`, and
a failure with no specific cause is reported as `execution_failed`. New codes arrive
without a major version, so treat an unrecognized one as `execution_failed`.

The sandbox reaches only `account_invalid` (`0001`), `compliance_rejected`
(`0004`) and `returned_by_bank` (`0003`). The rest, and `stage`, come from live
rails only, so write a branch for every code. Errors that refuse a request
before a payout exists are on [Errors](/errors/).

## Batch payout status

A [batch](/batch-payouts/) has its own statuses. They track creation, not
settlement:

```text
received → validating → (awaiting_confirmation | creating) → completed
canceled · failed        (terminal)
```

`completed` means every line became a payout or was refused; `counts` says
which. `failed` means the run itself broke. Each `created` line carries a
`payoutId`, and that payout follows the five statuses above.

## Approvals

With M-of-N approval on, a `POST /payouts` or batch `confirm` above the
threshold answers `202` with an approval instead of a payout. While it is open
there is no `payoutId` and nothing in `GET /orders`. When it executes, the
payout starts at `pending`.

```text
pending → approved → executing → executed          (the payout now exists; payoutId on the approval)
pending → rejected · expired                        (terminal; nothing was created)
executing → execution_failed · execution_unknown    (terminal; nothing was created, or resolved by hand)
```

Read approvals at `GET …/payouts/approvals` and `…/approvals/{approvalId}`.
Every transition except into `executing` and `execution_unknown` is also a
`payout_approval.*` event, and `payout_approval.executed` carries the
`payoutId`. An `owner` or `admin` approves or rejects in the dashboard, and a
`pending` approval expires after 24 hours.

## The informational `stage` property

Some rails add `stage`: `awaiting_details` or `under_review`. (The enum also
lists `settling`, which no payout carries today.) It answers "where is my payment?" for support; don't drive your ledger from it.
