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

# Payout lifecycle

> The seven statuses a payout moves through, and what drives each transition.

A payout advances through a fixed sequence. Statuses are never skipped and never
move backwards.

```mermaid theme={null}
flowchart LR
  A[CREATED] --> B[PENDING_PAYMENT]
  B --> C[PAYMENT_RECEIVED]
  B --> G[EXPIRED]
  C --> D[PROCESSING]
  D --> E[SUCCESSFUL]
  D --> F[FAILED]
```

| Status             | Meaning                                                    | Terminal |
| ------------------ | ---------------------------------------------------------- | -------- |
| `CREATED`          | Accepted, pre-flight checks passed. No instruction yet.    |          |
| `PENDING_PAYMENT`  | Rate locked, instruction issued. Awaiting your settlement. |          |
| `PAYMENT_RECEIVED` | Your settlement arrived and confirmed.                     |          |
| `PROCESSING`       | Dispatched to the local processor.                         |          |
| `SUCCESSFUL`       | The beneficiary received the funds.                        | Yes      |
| `FAILED`           | Delivery failed after retries were exhausted.              | Yes      |
| `EXPIRED`          | The instruction lapsed unpaid. No money ever arrived.      | Yes      |

## What moves a payout forward

<Steps>
  <Step title="CREATED → PENDING_PAYMENT">
    We lock an FX rate, compute the settlement amount and fees, and issue a
    payment instruction with an expiry. Generation starts as soon as the payout is
    created; a sweep every 30 seconds re-picks anything left behind.

    Until this runs, `settlement_amount` is `0` and `payment_instruction` is
    `null`. That is not an error — the amount is genuinely unknown until the rate
    is locked.
  </Step>

  <Step title="PENDING_PAYMENT → PAYMENT_RECEIVED">
    We match your settlement to the instruction by the account it was paid into
    and the amount. Crypto settlements additionally wait for network
    confirmations.

    Underpayment does **not** advance the payout — it is held for review rather
    than partially processed. Overpayment proceeds, and the excess is recorded and
    returned with any later refund.
  </Step>

  <Step title="PAYMENT_RECEIVED → PROCESSING">
    We dispatch the transfer to the processor. Each attempt is a separate record,
    so a retry never overwrites the history of a failed one — see
    [`GET /v1/payouts/{id}/outflows`](/api-reference/introduction).
  </Step>

  <Step title="PROCESSING → SUCCESSFUL or FAILED">
    We poll the processor until it reports a terminal outcome. A failed attempt
    with retries remaining returns the payout to `PAYMENT_RECEIVED` so a fresh
    attempt is dispatched; only once attempts are exhausted does it become
    `FAILED`.
  </Step>
</Steps>

## Expiry is terminal

If the instruction expires unpaid, the instruction is marked `EXPIRED` and the
payout ends at `EXPIRED`. **It is not regenerated and the rate is not re-locked.**
A merchant who misses the window creates a new payout.

<Note>
  A payout that is holding money is never expired — not on underpayment, and not
  while a crypto settlement is still confirming. Those stay in `PENDING_PAYMENT`
  for our operations team to resolve. `EXPIRED` only ever means *no funds arrived*,
  which is why it needs no refund.
</Note>

The default window is 30 minutes on the crypto rail and configurable per account
on the bank rail. Read `expires_at` off the instruction rather than assuming a
duration.

## When a payout fails

If a payout fails *after* your settlement was confirmed, the money is not lost. We
raise a refund and send it to the destination you registered for that settlement
currency, minus the cost of sending it back.

```mermaid theme={null}
flowchart LR
  A[payout failed] --> B[refund PENDING]
  B --> C[refund PROCESSING]
  C --> D[refund SUCCESSFUL]
```

The payout is marked `is_refunded: true` with a `refund_reference` as soon as the
refund is raised. There is no wallet and no balance to withdraw — see
[Refunds](/guides/refunds).

A payout that failed *before* any money arrived has nothing to return and raises no
refund.

## No cancellation

There is no cancel endpoint. A payout you no longer want simply goes unpaid and
lapses to `EXPIRED` at the instruction's expiry. Once you have settled it, it is
committed.

## The status timeline

`GET /v1/payouts/{id}` returns a `status_timeline`: every transition in order,
each with the reason it happened, what triggered it, and when.

```json theme={null}
{
  "id": "a3f1c2d4-...",
  "status": "SUCCESSFUL",
  "status_timeline": [
    {
      "from_status": null,
      "to_status": "CREATED",
      "reason_code": "PAYOUT_CREATED",
      "triggered_by": "API",
      "created_at": "2026-08-16T14:30:00.000Z"
    },
    {
      "from_status": "CREATED",
      "to_status": "PENDING_PAYMENT",
      "reason_code": "INSTRUCTION_GENERATED",
      "triggered_by": "SYSTEM",
      "triggered_by_id": "generate-payment-instructions",
      "created_at": "2026-08-16T14:30:04.000Z"
    },
    {
      "from_status": "PENDING_PAYMENT",
      "to_status": "PAYMENT_RECEIVED",
      "reason_code": "INFLOW_MATCHED",
      "triggered_by": "SYSTEM",
      "triggered_by_id": "match-inflows",
      "metadata": { "inflow_id": "b7e2d1a0-..." },
      "created_at": "2026-08-16T14:38:14.000Z"
    },
    {
      "from_status": "PAYMENT_RECEIVED",
      "to_status": "PROCESSING",
      "reason_code": "OUTFLOW_DISPATCHED",
      "triggered_by": "SYSTEM",
      "triggered_by_id": "dispatch-outflows",
      "metadata": { "outflow_id": "c1d2e3f4-...", "attempt_number": 1 },
      "created_at": "2026-08-16T14:38:45.000Z"
    },
    {
      "from_status": "PROCESSING",
      "to_status": "SUCCESSFUL",
      "reason_code": "PROCESSOR_CONFIRMED",
      "triggered_by": "SYSTEM",
      "created_at": "2026-08-16T14:39:02.000Z"
    }
  ]
}
```

Use this for support and reconciliation rather than inferring history from webhook
arrival times, which reflect delivery rather than the transition itself.

### Reason codes

| Code                    | Transition                                       |
| ----------------------- | ------------------------------------------------ |
| `PAYOUT_CREATED`        | → `CREATED`                                      |
| `INSTRUCTION_GENERATED` | → `PENDING_PAYMENT`                              |
| `INFLOW_MATCHED`        | → `PAYMENT_RECEIVED`                             |
| `INFLOW_CONFIRMED`      | Crypto settlement reached required confirmations |
| `OUTFLOW_DISPATCHED`    | → `PROCESSING`                                   |
| `PROCESSOR_CONFIRMED`   | → `SUCCESSFUL`                                   |
| `PROCESSOR_FAILED`      | A dispatch attempt failed; retries may remain    |
| `RETRIES_EXHAUSTED`     | → `FAILED`                                       |
| `INSTRUCTION_EXPIRED`   | → `EXPIRED`                                      |
| `REFUND_CREDITED`       | A refund was raised against the failed payout    |
| `SANCTIONS_BLOCKED`     | Blocked by screening                             |
| `ADMIN_OVERRIDE`        | Moved manually by our operations team            |

<Note>
  Status values are uppercase everywhere they appear — on the payout, in
  `status_timeline`, and inside webhook payloads. Webhook **event types** are the
  lowercase dotted form (`payout.pending_payment`), so the two are easy to mix up
  when you branch on them.
</Note>
