> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.crisscross.money/error-codes/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 }; }; 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. > Error responses across Authentication, Exchange, Collect, and Payouts