> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.crisscross.money/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.crisscross.money/_mcp/server.

# Error Codes

> Reference for the error response shapes and codes returned across CrissCross APIs, including the normalised provider-decline codes that Collect and Payouts share.

### Three error models, one set of HTTP conventions

CrissCross APIs share **HTTP-status semantics** — a `401` always means an auth problem, a `422` always means a business-rule violation — but the **error body shape** is defined per service domain. There are three distinct error models:

* **Authentication** (`/auth/oauth2/token`) returns a single-field envelope, `{ error }`, where `error` is a short prose description — not an OAuth 2.0 RFC error code.
* **Exchange** uses a structured envelope with a stable, machine-readable code enum (`QUOTE_EXPIRED`, `INSUFFICIENT_BALANCE`, …) and a `details` object.
* **Collect (payments), Payouts, and related services** use a flat envelope (`message`, `error`, `statusCode`) aligned with standard HTTP semantics. Branching is driven by the HTTP status, except where `error` carries a machine-readable code — several Collect rejections share a single status and are told apart only by that code. Provider-specific failure reasons are carried on the transaction's state, not on the HTTP error envelope. One exception: the `409 DUPLICATE_REFERENCE` response on session creation has its own body shape — see [Envelope](#envelope).

These are three deliberate, distinct approaches. If you integrate one service, read just its section.

### Shared HTTP-status semantics

Every CrissCross API uses standard REST status codes. Regardless of which service you call, the status code tells you the **category** of failure.

| Status                     | Category      | Meaning                                                       | Safe to retry?                                                                                               |
| -------------------------- | ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `400 Bad Request`          | Client error  | Request was malformed or failed schema validation.            | No — fix the request.                                                                                        |
| `401 Unauthorized`         | Auth          | Missing, invalid, or expired credentials.                     | No — re-authenticate.                                                                                        |
| `403 Forbidden`            | Auth          | Authenticated, but not permitted for this resource.           | No.                                                                                                          |
| `404 Not Found`            | Client error  | The referenced resource does not exist.                       | No.                                                                                                          |
| `409 Conflict`             | State         | Resource is in a state that does not allow this action.       | No — re-fetch state.                                                                                         |
| `422 Unprocessable Entity` | Business rule | Request was well-formed, but a business rule rejected it.     | No — adjust and resubmit.                                                                                    |
| `429 Too Many Requests`    | Throttling    | Rate limit hit. Respect the `Retry-After` header.             | Yes, with backoff.                                                                                           |
| `5xx`                      | Server        | Unexpected error on our side, or a downstream provider issue. | Yes, with backoff (plus `Idempotency-Key` where the endpoint supports it — see [Conventions](/conventions)). |

### Tracing failed requests

Every response produced by the API — success or failure, including `401`, `422`, `429`, and `500` — carries an `x-trace-id` response header (e.g. `trace-0197a3f1-2b4c-7d5e-8f90-1a2b3c4d5e6f`). It is generated server-side and is never echoed from the request.

Quote it when contacting support: it lets us trace the request end-to-end. The header is absent only when the request never reached the API — for example a malformed-JSON `400` or `413` rejected by the body parser, or an edge `502`/`503`.

---

## Authentication

The Authentication API (`POST /auth/oauth2/token`) implements the OAuth 2.0 Client Credentials flow, but its errors are **not** RFC 6749 error codes. Every error is a single-field envelope with a short prose description. This shape is **specific to the token endpoint** — once you have a token, the service you call next uses its own error envelope (see below).

### Envelope

```json
{
  "error": "Invalid client credentials"
}
```

### Reference

| HTTP | `error`                                    | Returned when                                                                |
| ---- | ------------------------------------------ | ---------------------------------------------------------------------------- |
| 400  | `Invalid request body`                     | The body is malformed JSON, or not JSON at all (e.g. form-encoded).          |
| 400  | `client_id and client_secret are required` | A required field is missing from the body.                                   |
| 401  | `Invalid client credentials`               | Unknown client or wrong secret — the two are deliberately indistinguishable. |
| 500  | `An unknown error occurred`                | Unexpected server-side error.                                                |
| 503  | `Authentication service unavailable`       | The authentication backend is unreachable. Retry with backoff.               |

The `error` string is a sentence for humans, not a stable machine code — branch on the HTTP status.

One more shape to know about: once you are calling other endpoints, a missing, invalid, or expired access token is rejected by the gateway with `401` and `{"message": "Unauthorized"}` — different again from the envelopes on this page. Treat it as the cue to request a fresh token.

---

## Exchange

The Exchange API returns a structured error envelope with a stable, machine-readable `code`, a human-readable `message`, and a `details` object whose shape varies per code.

### Envelope

```json
{
  "error": {
    "code": "QUOTE_EXPIRED",
    "message": "Quote qut_550e8400-e29b-41d4-a716-446655440000 expired at 2026-04-21T14:00:00Z",
    "details": {
      "quoteId": "qut_550e8400-e29b-41d4-a716-446655440000",
      "expiredAt": "2026-04-21T14:00:00Z"
    }
  }
}
```

| Field           | Type   | Description                                                      |
| --------------- | ------ | ---------------------------------------------------------------- |
| `error.code`    | string | Stable, machine-readable identifier. **Branch on this.**         |
| `error.message` | string | Human-readable description. For logs and display — do not parse. |
| `error.details` | object | Optional structured context. Shape depends on `code`.            |

`code` is part of the API contract. `message` and `details` may be refined over time.

### Code reference

Codes are grouped by the situation that produces them.

#### Request validation

| Code              | HTTP | Returned when                                                                                      |
| ----------------- | ---- | -------------------------------------------------------------------------------------------------- |
| `INVALID_REQUEST` | 400  | Request body, query, or headers failed validation. `details` typically lists the offending fields. |
| `INVALID_CURSOR`  | 400  | Pagination cursor is malformed or no longer valid. Restart pagination from the first page.         |

#### Authentication & authorization

| Code              | HTTP | Returned when                                                                                    |
| ----------------- | ---- | ------------------------------------------------------------------------------------------------ |
| `UNAUTHENTICATED` | 401  | `Authorization` header is missing, malformed, or the bearer token has expired.                   |
| `FORBIDDEN`       | 403  | Token is valid but lacks permission — most commonly, the resource belongs to a different client. |

#### Currency & rates

| Code                       | HTTP | Returned when                                                                                               |
| -------------------------- | ---- | ----------------------------------------------------------------------------------------------------------- |
| `CURRENCY_NOT_SUPPORTED`   | 400  | Requested currency is not an ISO 4217 code we trade.                                                        |
| `CURRENCY_NOT_PROVISIONED` | 403  | Currency is supported globally but not enabled on your account. Contact support to enable.                  |
| `CURRENCY_MISMATCH`        | 422  | Currencies in the request do not line up — e.g. a quote's `sellCurrency` does not match the source balance. |
| `RATE_NOT_AVAILABLE`       | 404  | No live rate is currently available for the requested pair. Retry briefly, or check market hours.           |
| `RATE_NOT_FOUND`           | 404  | A specific historical rate lookup did not match a recorded rate.                                            |

#### Quotes & orders

| Code                    | HTTP | Returned when                                                                                                                |
| ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------- |
| `QUOTE_NOT_FOUND`       | 404  | The `quoteId` does not exist, or the quote was garbage-collected after expiry.                                               |
| `QUOTE_EXPIRED`         | 409  | Quote was valid but has passed its `expiresAt`. Request a fresh quote.                                                       |
| `QUOTE_NOT_EXECUTABLE`  | 422  | Quote exists and is unexpired, but cannot be executed by this caller (e.g. wrong client, single-use quote already consumed). |
| `ORDER_NOT_FOUND`       | 404  | The `orderId` does not exist on your account.                                                                                |
| `ORDER_NOT_CANCELLABLE` | 409  | Order is in a state that cannot be cancelled (already filled, already cancelled, or settling).                               |

#### Balances & amounts

| Code                   | HTTP | Returned when                                                                                                               |
| ---------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------- |
| `INSUFFICIENT_BALANCE` | 422  | Account balance in the requested currency is below the requested amount. `details` includes `available` and `required`.     |
| `AMOUNT_OUT_OF_RANGE`  | 422  | Amount is below the minimum or above the maximum for this operation, currency, or rail. `details` includes `min` and `max`. |

#### Beneficiaries & deposits

| Code                     | HTTP | Returned when                                                                                   |
| ------------------------ | ---- | ----------------------------------------------------------------------------------------------- |
| `BENEFICIARY_NOT_FOUND`  | 404  | The `beneficiaryId` does not exist on your account.                                             |
| `BENEFICIARY_NOT_ACTIVE` | 422  | Beneficiary exists but is `pending`, `rejected`, or `disabled` — withdrawals to it are blocked. |
| `DEPOSIT_NOT_FOUND`      | 404  | The `depositId` does not exist or has not been credited.                                        |

Creating a beneficiary with an unsupported payment type (for example `swift` for USD), or without the `wireDetails` a USD wire requires, returns `INVALID_REQUEST` (400).

#### Withdrawals

| Code                         | HTTP | Returned when                                                                |
| ---------------------------- | ---- | ---------------------------------------------------------------------------- |
| `WITHDRAWAL_NOT_FOUND`       | 404  | The `withdrawalId` does not exist.                                           |
| `WITHDRAWAL_NOT_CANCELLABLE` | 409  | Withdrawal has already been sent to the rail and can no longer be cancelled. |

#### Idempotency

| Code                   | HTTP | Returned when                                                                  |
| ---------------------- | ---- | ------------------------------------------------------------------------------ |
| `IDEMPOTENCY_CONFLICT` | 409  | An `Idempotency-Key` was reused with a different request body. Pick a new key. |

### Recommended handling for Exchange

```typescript
type ExchangeError = {
  error: { code: string; message: string; details?: Record<string, unknown> };
};

function handle(status: number, body: ExchangeError) {
  switch (body.error.code) {
    case "UNAUTHENTICATED":
      return refreshTokenAndRetry();

    case "INSUFFICIENT_BALANCE":
    case "AMOUNT_OUT_OF_RANGE":
    case "QUOTE_EXPIRED":
      return surfaceToUser(body.error);

    case "INVALID_REQUEST":
    case "IDEMPOTENCY_CONFLICT":
      return logAndFail(body.error);

    default:
      if (status >= 500) return retryWithBackoff();
      return logAndFail(body.error);
  }
}
```

---

## Collect (payments) and Payouts

Collect (card, bank transfer, mobile money, Pay-by-Bank, Hosted Checkout, Direct API), Payouts, and the related surfaces (Verification, Reporting, Settlement, Payers, Rate Locks) share a single error envelope. This envelope is **different from Exchange's** by design: it is a flat shape aligned with standard HTTP semantics, where programmatic branching is driven by the HTTP status. Provider-specific failure reasons — card declines, mobile-money rejections, bank-transfer reversals — are carried on the transaction's state, not on the HTTP error envelope.

### Envelope

```json
{
  "message": "Phone number is required when pre-filled by merchant",
  "error": "Bad Request",
  "statusCode": 400
}
```

| Field        | Type    | Description                                                                                                                                                                                                                                                                  |
| ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message`    | string  | Human-readable description of what went wrong. For logs and end-user display — do not pattern-match.                                                                                                                                                                         |
| `error`      | string  | The error category. Usually the HTTP reason phrase (e.g. `"Bad Request"`, `"Unauthorized"`, `"Conflict"`, `"Unprocessable Entity"`). Some endpoints return a machine-readable snake\_case code instead — where one is present, branch on it rather than on the status alone. |
| `statusCode` | integer | The HTTP status code. Mirrors the response header so the body is self-describing; the response header remains authoritative.                                                                                                                                                 |

One Collect response departs from this envelope: `POST /checkout/session` rejects a reused `merchantReference` with `409` and the body `{ "code": "DUPLICATE_REFERENCE", "message": "…", "existingSessionId": "…" }` — there are no `error` or `statusCode` fields. Branch on `code`, and use `existingSessionId` to recover the original session (see [Payment Status & Recovery](/collect/payment-recovery)).

### How to branch

Drive control flow off the **HTTP response status**, then off `error` where it carries a machine-readable code. `message` is for logs, support tickets, and user-facing messaging — never pattern-match it.

```typescript
type PaymentsError = { message: string; error: string; statusCode: number };

// Several rejections share one HTTP status and are told apart only by `error`.
const MACHINE_CODES = new Set([
  "fx_not_enabled",
  "rate_lock_invalid",
  "rate_lock_not_found",
  "rate_lock_currency_mismatch",
]);

function handle(status: number, body: PaymentsError) {
  if (MACHINE_CODES.has(body.error)) return handleByCode(body.error);

  if (status === 401) return refreshTokenAndRetry();
  if (status === 429 || status >= 500) return retryWithBackoff();
  if (status === 409) return refetchAndDecide();   // e.g. session already has a transaction
  if (status === 404) return treatAsMissing();
  if (status >= 400 && status < 500) return surfaceToUser(body.message);
  return null;
}
```

### Provider error details on the transaction

Generic HTTP failures (validation, auth, missing resource, conflict) are conveyed by HTTP status + a clear `message` in the envelope above. **Payment-method-specific failures** — card declines, mobile-money rejections, bank-transfer reversals — are different: they don't surface as HTTP errors. The request that initiates a payment returns **`201 Created`** with the transaction's current status — usually `PENDING`, though a fast decline can come back synchronously as `FAILED` or `ERRORED`. Handle any status. Later updates arrive by polling `GET /v1/payment/{transactionId}` or via webhooks, and the provider's response is attached to the transaction's **`authState`**, with the provider's own code and message included verbatim.

The error/failed state on a transaction carries these fields:

| Field                     | Type   | Description                                                                                                                                                                                                                                                                                                                         |
| ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state`                   | string | The terminal state, e.g. `failed` or `error`.                                                                                                                                                                                                                                                                                       |
| `message`                 | string | Normalised, human-readable summary of the failure.                                                                                                                                                                                                                                                                                  |
| `code`                    | string | The normalised CrissCross code. Either an `AuthFailureCode` (for provider declines, e.g. `payinInsufficientFunds`, `invalidCard`, `fraudBlocked`) or an `ErrorCode` (for system errors, e.g. `downstreamError`). See the [Normalised code reference](#normalised-code-reference-collect--payouts) below for the full set. Optional. |
| `connectorFailureCode`    | string | The **raw error code returned by the underlying provider**, passed through unchanged. Use this for provider-specific handling, reconciliation, or to match against a provider's own documentation. Optional.                                                                                                                        |
| `connectorFailureMessage` | string | The **raw error message from the underlying provider**, passed through unchanged. Optional.                                                                                                                                                                                                                                         |
| `transitionedAt`          | string | ISO 8601 timestamp of when the transaction entered this state.                                                                                                                                                                                                                                                                      |

Example — a card payment declined for insufficient funds, as returned by `GET /v1/payment/{transactionId}` (or synchronously from `POST /v1/payment` when the decline is immediate):

```json
{
  "transactionId": "txn_01HV9N4Y3W5K2J0F8E2D7C1B0A",
  "status": "FAILED",
  "authState": {
    "state": "failed",
    "transitionedAt": "2026-05-13T10:30:42Z",
    "message": "Insufficient balance for transaction",
    "code": "payinInsufficientFunds",
    "connectorFailureCode": "2007",
    "connectorFailureMessage": "Insufficient funds"
  }
}
```

This lets your integration do both:

* Branch on the **normalised `code`** (the `AuthFailureCode` / `ErrorCode` enum below) for consistent cross-provider handling.
* Surface or log the **raw `connectorFailureCode` / `connectorFailureMessage`** when you need the provider's own vocabulary — for reconciliation, support tickets, or showing the cardholder a network-specific reason.

The same shape applies to:

* The synchronous response from the payment-initiation endpoint.
* The transaction resource retrieved via polling.
* Webhook payloads carrying transaction state updates.

### Normalised code reference (Collect & Payouts)

The values below are the complete set of normalised codes returned in `authState.code` across every payment method and payout rail. They split into two enums:

* **`AuthFailureCode`** — the underlying provider, acquirer, or payer rejected the transaction. Set when `authState.state` is `failed`.
* **`ErrorCode`** — a system, configuration, validation, or compliance problem prevented the transaction from being processed. Set when `authState.state` is `error`.

Codes are camelCase on the wire. Codes prefixed `payin` are returned only on Collect (payments) transactions; codes prefixed `payout` are returned only on Payouts transactions; all others can appear on either.

#### `AuthFailureCode` — provider declines and rejections

**3-D Secure failures (card only)**

| Code                | Returned when                                                                               |
| ------------------- | ------------------------------------------------------------------------------------------- |
| `3dsDeclinedByUser` | The cardholder cancelled 3-D Secure authentication.                                         |
| `3dsLookupFailed`   | 3-D Secure enrollment lookup failed at the issuer or scheme.                                |
| `3dsNotCompleted`   | 3-D Secure authentication started but did not complete (e.g. timeout, abandoned challenge). |

**Generic declines**

| Code               | Returned when                                                                                                         |
| ------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `acquirerRejected` | The acquirer rejected the transaction.                                                                                |
| `customerRejected` | The payer or recipient rejected the transaction (e.g. declined a mobile-money prompt, refused a pay-by-bank consent). |
| `declined`         | Provider declined the transaction without a more specific reason. Returned as a fallback when no other code applies.  |
| `accountNotActive` | The payer's or recipient's account exists but is suspended, dormant, or otherwise inactive.                           |
| `fraudBlocked`     | The transaction was blocked for fraud — either by the provider, the scheme, or CrissCross fraud screening.            |

**Insufficient funds**

| Code                     | Scope   | Returned when                                                    |
| ------------------------ | ------- | ---------------------------------------------------------------- |
| `payinInsufficientFunds` | Collect | The payer has insufficient funds in the funding account or card. |

**Invalid data, cards, and PINs**

| Code                            | Returned when                                                                                                                |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `invalidCard`                   | Card number, expiry, or CVV is rejected as invalid by the issuer or scheme.                                                  |
| `invalidData`                   | One or more transaction fields were rejected as invalid by the provider (shape was correct, but the value was unacceptable). |
| `invalidPin`                    | PIN entered for a card-present or PIN-protected flow was incorrect.                                                          |
| `payinPayerInvalidAccount`      | Collect: the payer's account identifier (e.g. phone number, bank account) was not recognised by the provider.                |
| `payoutRecipientInvalidAccount` | Payouts: the recipient's account identifier was not recognised by the destination rail.                                      |

**Limits exceeded**

| Code                           | Scope   | Returned when                                                                                   |
| ------------------------------ | ------- | ----------------------------------------------------------------------------------------------- |
| `payinPayerLimitExceeded`      | Collect | The payer hit a transaction, daily, or periodic limit imposed by their issuer, bank, or wallet. |
| `payinMerchantLimitExceeded`   | Collect | The merchant hit a transaction, daily, or periodic limit on its acquiring account.              |
| `payoutRecipientLimitExceeded` | Payouts | The recipient hit a credit limit on the destination rail (e.g. mobile-money wallet cap).        |

**Timeouts and conflicts**

| Code                         | Returned when                                                                                                                                                                                                                                                                                                       |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `timeout`                    | The payer or provider did not respond within the allowed time window; the transaction ends `FAILED`. Confirm the final state via polling or webhook before starting a new attempt — a debit that lands afterwards is handled by the late-payment flow (see [Webhook Events](/webhook-events#late-payment-summary)). |
| `transactionNotFound`        | A follow-up action referenced a transaction the provider has no record of.                                                                                                                                                                                                                                          |
| `pendingTransactionConflict` | A new transaction was attempted while an earlier one for the same payer or session is still pending.                                                                                                                                                                                                                |

**Deprecated codes**

These five remain in the enum for older transactions and rails, but new integrations should branch on the granular `payin*` / `payoutRecipient*` codes above instead:

| Code                   | Deprecated in favour of                                      |
| ---------------------- | ------------------------------------------------------------ |
| `insufficientFunds`    | `payinInsufficientFunds`                                     |
| `invalidAccount`       | `payinPayerInvalidAccount` / `payoutRecipientInvalidAccount` |
| `limitExceeded`        | `payinPayerLimitExceeded` / `payoutRecipientLimitExceeded`   |
| `dailyLimitExceeded`   | `payinPayerLimitExceeded` / `payoutRecipientLimitExceeded`   |
| `monthlyLimitExceeded` | `payinPayerLimitExceeded` / `payoutRecipientLimitExceeded`   |

#### `ErrorCode` — system, validation, and compliance errors

**System and processing**

| Code                 | Returned when                                                                                                                                                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `configurationError` | A required configuration was missing or malformed (e.g. unconfigured rail, missing credentials). Contact support.                                                                                                         |
| `decryptionFailed`   | A payload that should have been encrypted (e.g. encrypted card data) could not be decrypted.                                                                                                                              |
| `downstreamError`    | The underlying provider returned an error that did not map to a more specific code. Inspect `connectorFailureCode`/`connectorFailureMessage` for the provider's own reason.                                               |
| `internalError`      | An unexpected error on the CrissCross side. Terminal for that transaction — do not retry it. Before a new payment attempt, confirm the payment's actual state via [Payment Status & Recovery](/collect/payment-recovery). |
| `networkError`       | A network failure between CrissCross and the underlying provider. The transaction's terminal state is unknown — reconcile via polling or webhook.                                                                         |
| `systemError`        | A generic system-level failure. Handle as `internalError`.                                                                                                                                                                |

**Validation**

| Code              | Returned when                                                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `validationError` | A business-rule validation rejected the transaction (e.g. a required field was missing for the chosen rail, an enum value was unsupported). |
| `invalidCurrency` | The supplied currency is not supported on the chosen rail or method.                                                                        |
| `invalidAmount`   | The supplied amount is malformed or otherwise unacceptable independent of any min/max.                                                      |
| `amountTooSmall`  | The amount is below the minimum supported on the chosen rail, method, or currency.                                                          |
| `amountTooLarge`  | The amount is above the maximum supported on the chosen rail, method, or currency.                                                          |

**Capability gating**

| Code                      | Scope   | Returned when                                                                                                    |
| ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
| `payinsNotAllowed`        | Collect | The merchant is not enabled to accept payments via this rail, currency, or geography. Contact support to enable. |
| `payoutsNotAllowed`       | Payouts | The merchant is not enabled to issue payouts via this rail, currency, or geography. Contact support to enable.   |
| `payoutInsufficientFunds` | Payouts | The merchant's balance is insufficient to fund the requested payout.                                             |

**Compliance screening**

| Code                | Returned when                                                                                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `screeningRejected` | Sanctions, AML, or fraud screening blocked the transaction. The specific list, rule, or reason is not exposed in the response. Contact support if you believe this was in error. |
| `screeningTimeout`  | Screening could not complete within the allowed window. The transaction was not processed; you may retry.                                                                        |
| `screeningError`    | An error occurred during screening. Contact support if this persists.                                                                                                            |

For guidance on which codes are most common per payment method, and the user-facing messaging appropriate for each, see the per-method guides:

* [Card](/payment-method-card)
* [Bank Transfer](/payment-method-bank-transfer)
* [Pay By Bank](/payment-method-pbb-sa)
* [Mobile Money](/payment-method-mobile-money)
* [Refunds](/refunds)

---

### Rules of thumb (any service)

* **Never branch on `message`.** Message strings are for humans and may change without notice.
* **HTTP status is always part of the contract.** For Exchange, you also have a stable `error.code`. For Collect and Payouts, the HTTP status is the primary signal on the request response, and the normalised `authState.code` is the contract on the transaction.
* **Retry only `429` and `5xx`,** with exponential backoff. Retrying `4xx` blindly will fail again the same way.
* **Guard retries against duplicates.** Use `Idempotency-Key` on the endpoints that support it (see [Conventions](/conventions)). On Collect, reuse the same `merchantReference` per order and reconcile via [Search Payments](/collect/payment-recovery); on Payouts, retry with the same `merchantReference` — a `409 DUPLICATE_REFERENCE` confirms the original was accepted.
* **Log the full error body** on every failure, even codes you don't recognise — it's the fastest path to a support resolution.