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

# Errors

> Branch on terminal, retriable and ambiguous — never on HTTP status alone.

Every AptaPay error response has the same shape:

```json theme={null}
{
  "code": 422,
  "message": "The provider refused the payout.",
  "error": {
    "code": "payout_rejected_by_provider",
    "message": "The provider refused the payout.",
    "terminal": true,
    "retriable": false,
    "ambiguous": false,
    "details": { "...": "..." }
  }
}
```

`error.code` is a stable machine string. Build your logic against it and
against the three booleans — never against the HTTP status alone, and never
by string-matching `message`. Message text can change without notice;
`error.code` will not.

## The three booleans

**Exactly one of the three is always true.** This is the whole contract for
safe retries.

<AccordionGroup>
  <Accordion title="terminal: true — a definitive outcome" icon="circle-x">
    The request reached a final answer. Retrying with the same idempotency key
    cannot change it.

    **What to do:** if it was a debit-adjacent operation, reverse your own
    side and surface the failure to your user. Do not retry.
  </Accordion>

  <Accordion title="retriable: true — nothing moved" icon="rotate-cw">
    Safe to retry. Nothing has been applied on our side or the provider's.

    **What to do:** retry with backoff, using the **same** idempotency key.
    Never mint a new key for a retriable failure — a new key restarts the
    whole operation as if it were unrelated.
  </Accordion>

  <Accordion title="ambiguous: true — genuinely unknown" icon="circle-question-mark">
    The outcome is unknown. The request may or may not have been applied by
    the provider.

    **What to do: do not reverse anything.** Poll
    `GET /v1/collections/{reference}` or `GET /v1/payouts/{reference}` until it
    resolves to a terminal status, or escalate to manual review after a
    reasonable timeout. Reversing an ambiguous outcome is exactly how a
    double-processing or a lost debit happens.
  </Accordion>
</AccordionGroup>

<Warning>
  The single most damaging integration bug in a payments API is treating
  `ambiguous` as `terminal` and reversing. If you write only one branch
  carefully, write this one.
</Warning>

## Handling the three cases

```typescript handle-error.ts theme={null}
async function callWithRetry(fn: () => Promise<Response>, reference?: string) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fn();
    if (res.ok) return res.json();

    const { error } = await res.json();

    if (error.terminal) {
      // Final. Reverse our own side, tell the user, stop.
      throw new TerminalFailure(error.code, error.message);
    }

    if (error.retriable) {
      // Same idempotency key, exponential backoff.
      await sleep(2 ** attempt * 250);
      continue;
    }

    if (error.ambiguous) {
      // Do NOT reverse. Resolve by polling instead.
      return pollUntilTerminal(reference);
    }
  }
  throw new Error("retries exhausted");
}
```

## Common codes

| Class     | Codes you will actually see                                                                                                                                                                                                          |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Terminal  | `validation_failed`, `corridor_unsupported`, `amount_below_minimum`, `amount_above_maximum`, `payout_rejected_by_provider`, `charge_rejected_by_provider`, `bank_account_unresolvable`, `quote_expired`, `insufficient_tenant_float` |
| Ambiguous | `provider_unavailable`, `provider_timeout`, `provider_bad_response`, `internal_error`                                                                                                                                                |
| Retriable | `request_in_progress`, `velocity_limit_exceeded`, `rate_limited`, `global_ceiling_reached`                                                                                                                                           |

A few worth calling out:

* **`insufficient_tenant_float`** — your float balance in that currency does
  not cover the amount plus the estimated fee. Top up; the request itself was
  well-formed.
* **`velocity_limit_exceeded`** — you crossed a per-hour, per-day or
  per-destination cap. Retriable, but retrying immediately will fail again;
  back off meaningfully.
* **`idempotency_key_reused`** — the same key with *different* parameters.
  This is a bug or an attack on your side, never a legitimate retry. See
  [Idempotency](/concepts/idempotency).
* **`mode_mismatch`** — your body claimed a mode that disagrees with the key
  you signed with. Mode comes from the key. See [Modes](/concepts/modes).

## Status codes

| Status | Meaning                                                                                                                                                                      |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Malformed request, or `mode_mismatch`.                                                                                                                                       |
| `401`  | Any signing failure. Deliberately carries no detail — see [401 triage](/authentication#401-triage).                                                                          |
| `404`  | Unknown reference.                                                                                                                                                           |
| `422`  | Well-formed but rejected: validation, limits, float, idempotency conflict.                                                                                                   |
| `429`  | Rate limited.                                                                                                                                                                |
| `501`  | Not implemented for your routed provider — for example refunds. Check [Capabilities](/concepts/capabilities) at integration time rather than discovering this in production. |
| `503`  | Provider unavailable. Usually ambiguous — check the flags.                                                                                                                   |
