Error codes
CROSS Pay errors fall into two categories: SDK/runtime errors and REST API errors. When possible, the browser SDK surfaces structured errors in the following shape.
{
type: 'validation_error',
code: 'INVALID_REDIRECT_URL',
message: 'redirect URL is not allowed',
retryable: false,
requestId: 'req_...',
checkoutId: 'cs_...'
}
SDK error types
| type | Description | Default handling |
|---|---|---|
network_error | CDN/runtime/API network failure | Show a retry button when retryable |
validation_error | Validation failure on request values, URLs, currency/country | Prompt the user to correct input |
payment_error | Payment declined, expired, cancelled, or on-chain failure | Prompt to change payment method or start a new session |
authentication_error | API key/session authentication failure | Check server configuration |
rate_limit_error | Request quota exceeded | Apply Retry-After or backoff |
server_error | CROSS Pay or provider server error | Retry after a short delay |
sdk_error | SDK load or widget lifecycle error | Check integration code and CSP |
Network / SDK load
| code | retryable | Description |
|---|---|---|
NETWORK_TIMEOUT | ✓ | Network request did not complete within the timeout |
NETWORK_OFFLINE | ✓ | Browser is offline |
SDK_NOT_LOADED | ✓ | Runtime script is not yet ready |
WIDGET_LOAD_FAILED | ✓ | Widget iframe/runtime failed to load |
SSR_UNSUPPORTED | Browser-only payment method invoked from a server environment | |
INVALID_SCRIPT_URL | Disallowed cross-pay.js URL | |
HTTP_NOT_ALLOWED | HTTP used for a production script or redirect URL |
At the SDK class level you can distinguish ScriptLoadFailedError, ScriptLoadTimeoutError, ScriptLoadAbortedError, NamespaceNotAvailableError, and CrossPaySDKError.
Validation
| code | Description |
|---|---|
MISSING_PARAM | Required parameter missing |
INVALID_FORMAT | Format validation failed |
INVALID_AMOUNT | Amount is zero, negative, or outside the allowed range |
INVALID_CURRENCY | Currency format is not valid |
UNSUPPORTED_CURRENCY | Currency is not supported by CROSS Pay |
UNSUPPORTED_COUNTRY | Country is unsupported or disabled |
INVALID_RESPONSE | Runtime/API response shape did not match expectations |
INVALID_REDIRECT_URL | Unsafe redirect URL or not on the allowlist |
Payment
| code | retryable | Description |
|---|---|---|
CHECKOUT_EXPIRED | Checkout session TTL expired. A new POST /v1/payments is required | |
PAYMENT_DECLINED | The provider declined the payment | |
PAYMENT_CANCELLED | The user cancelled the payment or closed the window | |
USER_REJECTED | Normalized SDK code for user cancellation | |
PAYMENT_FAILED | Generic payment failure | |
INSUFFICIENT_FUNDS | Insufficient balance | |
POLLING_TIMEOUT | ✓ | Status check did not complete within the timeout |
POPUP_BLOCKED | Browser blocked the popup | |
WALLET_INIT_FAILED | ✓ | Embedded wallet initialization failed |
WRONG_CHAIN | Wallet network differs from the payment chain | |
ONCHAIN_TX_FAILED | On-chain transaction failed or reverted |
Widget lifecycle
| code | Description |
|---|---|
AMOUNT_NOT_SET | Submit attempted before widgets.setAmount() was called |
WIDGET_NOT_MOUNTED | submit/update called while the payment method widget is not mounted |
WIDGET_LOAD_FAILED | Widget iframe failed to load or bridge failed to initialize |
Representative REST API codes
REST API error responses use the shape { code, message }, defined in
internal/api/errors.go (cross-pay-api).
4xx (client-side errors)
| code | HTTP | Description |
|---|---|---|
BAD_REQUEST | 400 | Validation failure on request body/query/header |
INVALID_AMOUNT | 400 | Invalid amount format or exceeds currency decimals |
INVALID_CURRENCY | 400 | Invalid or unsupported currency code |
UNAUTHORIZED | 401 | Missing or invalid merchant API key |
FORBIDDEN | 403 | Attempted access to another merchant's resource |
NOT_FOUND | 404 | Target resource not found |
CHECKOUT_NOT_FOUND | 404 | Checkout id not found |
PAYMENT_METHOD_NOT_FOUND | 404 | Selected payment method does not exist or is inactive |
CONFLICT | 409 | Generic conflict |
IDEMPOTENCY_CONFLICT | 409 | Same Idempotency-Key reused with a different request body |
INVALID_CHECKOUT_STATE | 409 | SelectQuote called on a terminal-state checkout |
INVALID_PAYMENT_STATE | 409 | Operation not allowed in the current payment state (e.g., cancel on PAID) |
INVALID_TRANSITION | 409 | State transition violation. e.g., cancel on PROCESSING payment — to avoid races with the PG |
CHECKOUT_EXPIRED | 410 | Checkout TTL expired — a new POST /v1/payments is required |
SESSION_GONE | 410 | On-chain session ended or payment reached a terminal state |
UNPROCESSABLE | 422 | Request is syntactically valid but semantically unprocessable |
UNSUPPORTED_CURRENCY | 422 | Currency not supported for the payment method/country combination |
UNSUPPORTED_COUNTRY | 422 | Unregistered or inactive country |
PAYMENT_TOKEN_NOT_AVAILABLE | 422 | Token mapping is not enabled for the chain |
PG_NOT_AVAILABLE | 422 | PG for the selected payment method is not available for this checkout |
REFUND_EXCEEDS_REMAINING | 409 | Refund amount exceeds remaining refundable amount |
REFUND_PROVIDER_UNAVAILABLE | 503 | PG refund channel is temporarily unavailable |
RATE_LIMITED | 429 | Request quota exceeded |
5xx (server-side errors)
| code | HTTP | Description |
|---|---|---|
INTERNAL_ERROR | 500 | Internal server error |
SERVER_ERROR | 500 | (legacy alias) INTERNAL_ERROR |
NOT_IMPLEMENTED | 501 | Route is exposed but not yet implemented |
PG_UPSTREAM_ERROR | 502 | Upstream provider error |
SERVICE_UNAVAILABLE | 503 | Temporary service unavailability |
INVALID_TRANSITION vs INVALID_PAYMENT_STATEBoth codes are 409 but carry different intent.
INVALID_TRANSITION— state machine violation (e.g., attemptingPROCESSING → CANCELLED). For the full lifecycle, see Webhooks Overview.INVALID_PAYMENT_STATE— the operation itself does not fit the payment state (e.g., attempting a mutation rather than a simple lookup onPAID).
Merchants typically handle both codes uniformly as "not allowed in the current state" rather than branching on each one separately.
FDS deny
/v1/checkout/** responds with HTTP 200 even when FDS blocks the request. The body omits Payment/Quote objects and includes only the fds envelope.
{
"fds": {
"outcome": "DENY",
"code": "COUNTRY_BLOCKED",
"message": "Payment is blocked by risk policy",
"risk_level": "HIGH",
"risk_score": 92,
"rule": "country_allowlist"
}
}
The SDK and checkout UI must branch on fds.outcome, not the HTTP status. Treat unknown fds.code values as a generic block.