Error Codes
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 }, whereerroris 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 adetailsobject. - 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 whereerrorcarries 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: the409 DUPLICATE_REFERENCEresponse on session creation has its own body shape — see 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.
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
Reference
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
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
Authentication & authorization
Currency & rates
Quotes & orders
Balances & amounts
Beneficiaries & deposits
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
Idempotency
Recommended handling for Exchange
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
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).
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.
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:
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):
This lets your integration do both:
- Branch on the normalised
code(theAuthFailureCode/ErrorCodeenum below) for consistent cross-provider handling. - Surface or log the raw
connectorFailureCode/connectorFailureMessagewhen 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 whenauthState.stateisfailed.ErrorCode— a system, configuration, validation, or compliance problem prevented the transaction from being processed. Set whenauthState.stateiserror.
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)
Generic declines
Insufficient funds
Invalid data, cards, and PINs
Limits exceeded
Timeouts and conflicts
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:
ErrorCode — system, validation, and compliance errors
System and processing
Validation
Capability gating
Compliance screening
For guidance on which codes are most common per payment method, and the user-facing messaging appropriate for each, see the per-method guides:
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 normalisedauthState.codeis the contract on the transaction. - Retry only
429and5xx, with exponential backoff. Retrying4xxblindly will fail again the same way. - Guard retries against duplicates. Use
Idempotency-Keyon the endpoints that support it (see Conventions). On Collect, reuse the samemerchantReferenceper order and reconcile via Search Payments; on Payouts, retry with the samemerchantReference— a409 DUPLICATE_REFERENCEconfirms 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.