Skip to navigation

Error Codes

Error responses across Authentication, Exchange, Collect, and Payouts

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.

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.

StatusCategoryMeaningSafe to retry?
400 Bad RequestClient errorRequest was malformed or failed schema validation.No — fix the request.
401 UnauthorizedAuthMissing, invalid, or expired credentials.No — re-authenticate.
403 ForbiddenAuthAuthenticated, but not permitted for this resource.No.
404 Not FoundClient errorThe referenced resource does not exist.No.
409 ConflictStateResource is in a state that does not allow this action.No — re-fetch state.
422 Unprocessable EntityBusiness ruleRequest was well-formed, but a business rule rejected it.No — adjust and resubmit.
429 Too Many RequestsThrottlingRate limit hit. Respect the Retry-After header.Yes, with backoff.
5xxServerUnexpected error on our side, or a downstream provider issue.Yes, with backoff (plus Idempotency-Key where the endpoint supports it — see 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

{
"error": "Invalid client credentials"
}

Reference

HTTPerrorReturned when
400Invalid request bodyThe body is malformed JSON, or not JSON at all (e.g. form-encoded).
400client_id and client_secret are requiredA required field is missing from the body.
401Invalid client credentialsUnknown client or wrong secret — the two are deliberately indistinguishable.
500An unknown error occurredUnexpected server-side error.
503Authentication service unavailableThe 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

{
"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"
}
}
}
FieldTypeDescription
error.codestringStable, machine-readable identifier. Branch on this.
error.messagestringHuman-readable description. For logs and display — do not parse.
error.detailsobjectOptional 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

CodeHTTPReturned when
INVALID_REQUEST400Request body, query, or headers failed validation. details typically lists the offending fields.
INVALID_CURSOR400Pagination cursor is malformed or no longer valid. Restart pagination from the first page.

Authentication & authorization

CodeHTTPReturned when
UNAUTHENTICATED401Authorization header is missing, malformed, or the bearer token has expired.
FORBIDDEN403Token is valid but lacks permission — most commonly, the resource belongs to a different client.

Currency & rates

CodeHTTPReturned when
CURRENCY_NOT_SUPPORTED400Requested currency is not an ISO 4217 code we trade.
CURRENCY_NOT_PROVISIONED403Currency is supported globally but not enabled on your account. Contact support to enable.
CURRENCY_MISMATCH422Currencies in the request do not line up — e.g. a quote’s sellCurrency does not match the source balance.
RATE_NOT_AVAILABLE404No live rate is currently available for the requested pair. Retry briefly, or check market hours.
RATE_NOT_FOUND404A specific historical rate lookup did not match a recorded rate.

Quotes & orders

CodeHTTPReturned when
QUOTE_NOT_FOUND404The quoteId does not exist, or the quote was garbage-collected after expiry.
QUOTE_EXPIRED409Quote was valid but has passed its expiresAt. Request a fresh quote.
QUOTE_NOT_EXECUTABLE422Quote exists and is unexpired, but cannot be executed by this caller (e.g. wrong client, single-use quote already consumed).
ORDER_NOT_FOUND404The orderId does not exist on your account.
ORDER_NOT_CANCELLABLE409Order is in a state that cannot be cancelled (already filled, already cancelled, or settling).

Balances & amounts

CodeHTTPReturned when
INSUFFICIENT_BALANCE422Account balance in the requested currency is below the requested amount. details includes available and required.
AMOUNT_OUT_OF_RANGE422Amount is below the minimum or above the maximum for this operation, currency, or rail. details includes min and max.

Beneficiaries & deposits

CodeHTTPReturned when
BENEFICIARY_NOT_FOUND404The beneficiaryId does not exist on your account.
BENEFICIARY_NOT_ACTIVE422Beneficiary exists but is pending, rejected, or disabled — withdrawals to it are blocked.
DEPOSIT_NOT_FOUND404The 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

CodeHTTPReturned when
WITHDRAWAL_NOT_FOUND404The withdrawalId does not exist.
WITHDRAWAL_NOT_CANCELLABLE409Withdrawal has already been sent to the rail and can no longer be cancelled.

Idempotency

CodeHTTPReturned when
IDEMPOTENCY_CONFLICT409An Idempotency-Key was reused with a different request body. Pick a new key.
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

{
"message": "Phone number is required when pre-filled by merchant",
"error": "Bad Request",
"statusCode": 400
}
FieldTypeDescription
messagestringHuman-readable description of what went wrong. For logs and end-user display — do not pattern-match.
errorstringThe 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.
statusCodeintegerThe 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).

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.

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:

FieldTypeDescription
statestringThe terminal state, e.g. failed or error.
messagestringNormalised, human-readable summary of the failure.
codestringThe 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 below for the full set. Optional.
connectorFailureCodestringThe 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.
connectorFailureMessagestringThe raw error message from the underlying provider, passed through unchanged. Optional.
transitionedAtstringISO 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):

{
"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)

CodeReturned when
3dsDeclinedByUserThe cardholder cancelled 3-D Secure authentication.
3dsLookupFailed3-D Secure enrollment lookup failed at the issuer or scheme.
3dsNotCompleted3-D Secure authentication started but did not complete (e.g. timeout, abandoned challenge).

Generic declines

CodeReturned when
acquirerRejectedThe acquirer rejected the transaction.
customerRejectedThe payer or recipient rejected the transaction (e.g. declined a mobile-money prompt, refused a pay-by-bank consent).
declinedProvider declined the transaction without a more specific reason. Returned as a fallback when no other code applies.
accountNotActiveThe payer’s or recipient’s account exists but is suspended, dormant, or otherwise inactive.
fraudBlockedThe transaction was blocked for fraud — either by the provider, the scheme, or CrissCross fraud screening.

Insufficient funds

CodeScopeReturned when
payinInsufficientFundsCollectThe payer has insufficient funds in the funding account or card.

Invalid data, cards, and PINs

CodeReturned when
invalidCardCard number, expiry, or CVV is rejected as invalid by the issuer or scheme.
invalidDataOne or more transaction fields were rejected as invalid by the provider (shape was correct, but the value was unacceptable).
invalidPinPIN entered for a card-present or PIN-protected flow was incorrect.
payinPayerInvalidAccountCollect: the payer’s account identifier (e.g. phone number, bank account) was not recognised by the provider.
payoutRecipientInvalidAccountPayouts: the recipient’s account identifier was not recognised by the destination rail.

Limits exceeded

CodeScopeReturned when
payinPayerLimitExceededCollectThe payer hit a transaction, daily, or periodic limit imposed by their issuer, bank, or wallet.
payinMerchantLimitExceededCollectThe merchant hit a transaction, daily, or periodic limit on its acquiring account.
payoutRecipientLimitExceededPayoutsThe recipient hit a credit limit on the destination rail (e.g. mobile-money wallet cap).

Timeouts and conflicts

CodeReturned when
timeoutThe 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).
transactionNotFoundA follow-up action referenced a transaction the provider has no record of.
pendingTransactionConflictA 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:

CodeDeprecated in favour of
insufficientFundspayinInsufficientFunds
invalidAccountpayinPayerInvalidAccount / payoutRecipientInvalidAccount
limitExceededpayinPayerLimitExceeded / payoutRecipientLimitExceeded
dailyLimitExceededpayinPayerLimitExceeded / payoutRecipientLimitExceeded
monthlyLimitExceededpayinPayerLimitExceeded / payoutRecipientLimitExceeded

ErrorCode — system, validation, and compliance errors

System and processing

CodeReturned when
configurationErrorA required configuration was missing or malformed (e.g. unconfigured rail, missing credentials). Contact support.
decryptionFailedA payload that should have been encrypted (e.g. encrypted card data) could not be decrypted.
downstreamErrorThe underlying provider returned an error that did not map to a more specific code. Inspect connectorFailureCode/connectorFailureMessage for the provider’s own reason.
internalErrorAn 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.
networkErrorA network failure between CrissCross and the underlying provider. The transaction’s terminal state is unknown — reconcile via polling or webhook.
systemErrorA generic system-level failure. Handle as internalError.

Validation

CodeReturned when
validationErrorA business-rule validation rejected the transaction (e.g. a required field was missing for the chosen rail, an enum value was unsupported).
invalidCurrencyThe supplied currency is not supported on the chosen rail or method.
invalidAmountThe supplied amount is malformed or otherwise unacceptable independent of any min/max.
amountTooSmallThe amount is below the minimum supported on the chosen rail, method, or currency.
amountTooLargeThe amount is above the maximum supported on the chosen rail, method, or currency.

Capability gating

CodeScopeReturned when
payinsNotAllowedCollectThe merchant is not enabled to accept payments via this rail, currency, or geography. Contact support to enable.
payoutsNotAllowedPayoutsThe merchant is not enabled to issue payouts via this rail, currency, or geography. Contact support to enable.
payoutInsufficientFundsPayoutsThe merchant’s balance is insufficient to fund the requested payout.

Compliance screening

CodeReturned when
screeningRejectedSanctions, 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.
screeningTimeoutScreening could not complete within the allowed window. The transaction was not processed; you may retry.
screeningErrorAn 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:


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). On Collect, reuse the same merchantReference per order and reconcile via Search Payments; 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.