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

# Status Lifecycle

> How a payout transitions from PENDING to a terminal state

Every payout is a small state machine. Knowing exactly which states are
legal — and how the mock advances between them — makes it easy to write a
polling loop, react to a webhook, or assert against the response in a test.

## States

| Status       | Terminal? | Meaning                                        |
| ------------ | --------- | ---------------------------------------------- |
| `PENDING`    | no        | Accepted, queued, not yet sent to "bank rails" |
| `PROCESSING` | no        | "In flight" to the beneficiary bank            |
| `PAID`       | **yes**   | Funds "delivered"                              |
| `REJECTED`   | **yes**   | Not paid — see `status_detail` / `status_code` |
| `CANCELLED`  | **yes**   | Cancelled by caller before completion          |

`PAID`, `REJECTED`, and `CANCELLED` are terminal — once a payout enters one of
these, its status never changes. A cancel call on a terminal payout returns
`409 PAYOUT_NOT_CANCELLABLE`.

## The full diagram

```
                ┌──────────┐
                │ PENDING  │ ──────► REJECTED   (intake reject)
                └────┬─────┘
                     │
                     ▼
                ┌────────────┐
                │ PROCESSING │ ────► PAID       (happy path)
                └────┬───────┘ ────► REJECTED   (bank reject)
                     │
                     ▼
                 CANCELLED          (caller cancels — from PENDING or PROCESSING)
```

## Transitions in detail

<Steps>
  <Step title="Create → PENDING">
    `POST /v1/payouts` returns `201` with `status: "PENDING"`. The mock
    schedules the rest of the state machine on a timer (default 2 seconds
    per step, overridable via `processing.step_seconds`).

    Some [magic triggers](/payouts/magic-triggers) skip this step and return
    `201` with a terminal status already applied — typically `MOCK_REJECT_INTAKE_`,
    amount-ending-in-`.99`, and `document_id == "00000000000"`.
  </Step>

  <Step title="PENDING → PROCESSING">
    The payout has "left the queue" and is "in flight". Default route always
    goes through this state, even when the final outcome is REJECTED or
    CANCELLED — observers see the full lifecycle.
  </Step>

  <Step title="PROCESSING → PAID">
    Happy path. `status_code: "200"`, `status_detail: "Payout received and queued."`
  </Step>

  <Step title="PROCESSING → REJECTED">
    Bank-side reject. `status_code` is one of:

    * `"301"` — beneficiary bank rejected the transfer
    * `"302"` — insufficient funds in payout account

    `status_detail` ends with `"(mock trigger)."` when the reject was forced
    by a magic trigger.
  </Step>

  <Step title="* → CANCELLED">
    `POST /v1/payouts/{id}/cancel` works from `PENDING` or `PROCESSING`. The
    payout transitions immediately. A cancel against a terminal payout returns
    `409 PAYOUT_NOT_CANCELLABLE`.
  </Step>
</Steps>

## When transitions actually fire

The mock advances payouts **two ways**, both routed through the same state
machine code:

1. **Per-payout timer** scheduled at create time. Default cadence: every
   2 seconds (server-configurable via `PAYOUTS_MOCK_STEP_SECONDS`). Override
   per request with `processing.step_seconds` (clamped to `[0, 60]`).
2. **Lazy advance on read.** `GET /v1/payouts/{id}` evaluates "where would
   this be by wall clock now?" and applies pending transitions before
   responding. This is why tests don't have to depend on a real clock.

`processing.step_seconds: 0` collapses the timer to "apply all transitions
synchronously before returning the create response." The `201` already
reflects the terminal state. **This is the recommended setting for tests**.

## Reading the result

The full lifecycle is preserved on the resource:

```json theme={null}
{
  "id": "po_01J...",
  "status": "PAID",
  "status_history": [
    { "status": "PENDING",    "at": "2026-05-12T15:04:05Z", "detail": "Payout received and queued." },
    { "status": "PROCESSING", "at": "2026-05-12T15:04:07Z" },
    { "status": "PAID",       "at": "2026-05-12T15:04:09Z" }
  ],
  "estimated_settlement_at": "2026-05-12T15:04:09Z"
}
```

`estimated_settlement_at` is set at create time to `created_at + N * step_seconds`,
where `N` is the length of the planned route. It's a hint, not a guarantee —
read `status` and `status_history` for the truth.

## Cancelling

```bash theme={null}
curl -X POST https://api.clevis.dev/v1/payouts/po_01J.../cancel \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Returns:

* `200 OK` with the cancelled `Payout` if the payout was `PENDING` or `PROCESSING`
* `409 PAYOUT_NOT_CANCELLABLE` if the payout was already terminal
* `404 PAYOUT_NOT_FOUND` if the id doesn't exist for your client

<Note>
  The mock allows cancelling from `PROCESSING`, which a real provider may not.
  This is intentional — it makes the cancellation path easy to exercise in
  demos.
</Note>

## Idle "stuck" state

The `MOCK_STUCK_` `external_id` prefix advances a payout to `PROCESSING` and
then **never settles**. Use it to exercise your polling-loop timeout / "stale
payout" UX without actually waiting for a real provider to misbehave. See
[Magic triggers](/payouts/magic-triggers).
