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) follows the OAuth 2.0 standard (RFC 6749 §5.2): { error, error_description, error_uri } with a fixed set of error values.
  • 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; provider-specific failure reasons are carried on the transaction’s state, not on the HTTP error 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 + Idempotency-Key.

Authentication

The Authentication API (POST /auth/oauth2/token) implements OAuth 2.0 Client Credentials and returns errors in the format defined by RFC 6749 §5.2. 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

1{
2 "error": "invalid_client",
3 "error_description": "Client authentication failed.",
4 "error_uri": "https://payos.money/docs/authentication"
5}
FieldRequiredDescription
erroryesA single ASCII error code from the OAuth 2.0 spec. Branch on this.
error_descriptionnoHuman-readable text providing additional information.
error_urinoURI pointing to a human-readable web page with more information.

Code reference

CodeHTTPReturned when
invalid_request400The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.
invalid_client401Client authentication failed — unknown client, no client authentication included, or unsupported authentication method.
invalid_grant400The supplied grant (e.g. authorization code, credentials) is invalid, expired, or revoked.
unauthorized_client400The authenticated client is not authorized to use this grant type.
unsupported_grant_type400The grant type requested is not supported by the authorization server.
invalid_scope400The requested scope is invalid, unknown, malformed, or exceeds the scope granted to the client.

For the Client Credentials flow used here, you will most commonly see invalid_client (bad credentials) and invalid_request (missing or malformed body).


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

1{
2 "error": {
3 "code": "QUOTE_EXPIRED",
4 "message": "Quote qut_550e8400-e29b-41d4-a716-446655440000 expired at 2026-04-21T14:00:00Z",
5 "details": {
6 "quoteId": "qut_550e8400-e29b-41d4-a716-446655440000",
7 "expiredAt": "2026-04-21T14:00:00Z"
8 }
9 }
10}
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.

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.
1type ExchangeError = {
2 error: { code: string; message: string; details?: Record<string, unknown> };
3};
4
5function handle(status: number, body: ExchangeError) {
6 switch (body.error.code) {
7 case "UNAUTHENTICATED":
8 return refreshTokenAndRetry();
9
10 case "INSUFFICIENT_BALANCE":
11 case "AMOUNT_OUT_OF_RANGE":
12 case "QUOTE_EXPIRED":
13 return surfaceToUser(body.error);
14
15 case "INVALID_REQUEST":
16 case "IDEMPOTENCY_CONFLICT":
17 return logAndFail(body.error);
18
19 default:
20 if (status >= 500) return retryWithBackoff();
21 return logAndFail(body.error);
22 }
23}

Collect (payments) and Payouts

Collect (card, bank transfer, mobile money, Pay-by-Bank, Hosted Checkout, Secure Fields, Direct API), Payouts, and the related surfaces (Verification, Reporting, Settlement, Payment Rules, Payers) 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

1{
2 "message": "Phone number is required when pre-filled by merchant",
3 "error": "Bad Request",
4 "statusCode": "400"
5}
FieldTypeDescription
messagestringHuman-readable description of what went wrong. For logs and end-user display — do not pattern-match.
errorstringThe error category — typically the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Conflict", "Unprocessable Entity", "Internal Server Error").
statusCodestringThe HTTP status code, as a string. Mirrors the response header so the body is self-describing; the response header remains authoritative.

How to branch

Drive control flow off the HTTP response status. The body fields provide context for logs, support tickets, and user-facing messaging — they are not intended for programmatic branching.

1type PaymentsError = { message: string; error: string; statusCode: string };
2
3function handle(status: number, body: PaymentsError) {
4 if (status === 401) return refreshTokenAndRetry();
5 if (status === 429 || status >= 500) return retryWithBackoff();
6 if (status === 409) return refetchAndDecide(); // e.g. session already has a transaction
7 if (status === 404) return treatAsMissing();
8 if (status >= 400 && status < 500) return surfaceToUser(body.message);
9 return null;
10}

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 even when the underlying provider declines the transaction. The provider’s response is attached to the transaction’s state instead, 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. PAYIN_INSUFFICIENT_FUNDS, INVALID_CARD, FRAUD_BLOCKED) or an ErrorCode (for system errors, e.g. DOWNSTREAM_ERROR). 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, returned as HTTP 201 from POST /payment:

1{
2 "transactionId": "txn_01HV9N4Y3W5K2J0F8E2D7C1B0A",
3 "status": "failed",
4 "authState": {
5 "state": "failed",
6 "transitionedAt": "2026-05-13T10:30:42Z",
7 "message": "Insufficient balance for transaction",
8 "code": "PAYIN_INSUFFICIENT_FUNDS",
9 "connectorFailureCode": "2007",
10 "connectorFailureMessage": "Insufficient funds"
11 }
12}

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 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
THREEDS_DECLINED_BY_USERThe cardholder cancelled 3-D Secure authentication.
THREEDS_LOOKUP_FAILED3-D Secure enrollment lookup failed at the issuer or scheme.
THREEDS_NOT_COMPLETED3-D Secure authentication started but did not complete (e.g. timeout, abandoned challenge).

Generic declines

CodeReturned when
ACQUIRER_REJECTEDThe acquirer rejected the transaction.
CUSTOMER_REJECTEDThe 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.
ACCOUNT_NOT_ACTIVEThe payer’s or recipient’s account exists but is suspended, dormant, or otherwise inactive.
FRAUD_BLOCKEDThe transaction was blocked for fraud — either by the provider, the scheme, or CrissCross fraud screening.

Insufficient funds

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

Invalid data, cards, and PINs

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

Limits exceeded

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

Timeouts and conflicts

CodeReturned when
TIMEOUTThe provider did not respond within the allowed time window. The transaction’s terminal state is unknown — reconcile via polling or webhook.
TRANSACTION_NOT_FOUNDA follow-up action referenced a transaction the provider has no record of.
PENDING_TRANSACTION_CONFLICTA new transaction was attempted while an earlier one for the same payer or session is still pending.

ErrorCode — system, validation, and compliance errors

System and processing

CodeReturned when
CONFIGURATION_ERRORA required configuration was missing or malformed (e.g. unconfigured rail, missing credentials). Contact support.
DECRYPTION_FAILEDA payload that should have been encrypted (e.g. card data from Secure Fields) could not be decrypted.
DOWNSTREAM_ERRORThe underlying provider returned an error that did not map to a more specific code. Inspect connectorFailureCode/connectorFailureMessage for the provider’s own reason.
INTERNAL_ERRORAn unexpected internal error. Safe to retry with backoff and an Idempotency-Key.
NETWORK_ERRORA network failure between CrissCross and the underlying provider. The transaction’s terminal state is unknown — reconcile via polling or webhook.
SYSTEM_ERRORA generic system-level failure. As with INTERNAL_ERROR, retry with backoff.

Validation

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

Capability gating

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

Compliance screening

CodeReturned when
SCREENING_REJECTEDSanctions, 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.
SCREENING_TIMEOUTScreening could not complete within the allowed window. The transaction was not processed; you may retry.
SCREENING_ERRORAn 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.
  • Use Idempotency-Key on every mutating call. It turns a 5xx retry from “did this just happen twice?” into a safe, repeatable operation.
  • Log the full error body on every failure, even codes you don’t recognise — it’s the fastest path to a support resolution.