Authentication
cross_merchant_forbidden
cross-merchant access forbidden · HTTP 403
Your credentials resolved successfully, but the resource identified in the request URL or body belongs to a different merchant. ChainMore enforces strict merchant-scope at the service boundary: credentials (API key or OIDC JWT) can only act on resources owned by the merchant they authenticate for.
What to do: Check that the resource id in the URL or body is one your merchant owns (listed under GET /v1/dashboard/...). If you operate multiple merchants, use the credentials scoped to the owning merchant. This is a boundary enforcement, not a transient failure, retrying with the same credentials and resource will always be refused.
dashboard_role_forbidden
dashboard role forbidden · HTTP 403
Your OIDC session resolved to the correct merchant, but your dashboard member role is not allowed to perform this action.
What to do: Ask a merchant owner or administrator to perform the action or update your dashboard role. Read-only users can still view the dashboard surfaces their role permits.
jwt_expired
jwt expired · HTTP 401 · safe to retry
The OIDC JWT presented as bearer has an exp (expiry) claim in the past, even after the configured clock-skew tolerance. Access tokens are short-lived (typically a few minutes); the client is expected to refresh.
What to do: Use your refresh token at Keycloak's token endpoint to mint a new access token, then retry. If you cannot refresh (browser closed, session ended), re-authenticate the user via the OIDC login flow.
jwt_invalid
jwt invalid · HTTP 401
The OIDC JWT presented as bearer failed validation. Possible reasons: bad signature, wrong iss (issuer), wrong aud (audience), malformed token, or a claim value that does not match this environment's expected set. ID-tokens presented to the API are rejected here, only OIDC access tokens are accepted.
What to do: Re-authenticate via the configured Keycloak realm, ensure your OIDC client is configured to request an access token (not an ID token) with aud = chainmore-gateway, and try again. For programmatic integration, use the API-key path instead of JWTs.
membership_missing
merchant membership missing · HTTP 403
The OIDC JWT is valid, but the authenticated user is not a member of any active merchant in this environment. A JWT identity must resolve to exactly one active row in the merchant_members table (keyed by issuer + subject) to authorize dashboard or control-plane actions.
What to do: Contact your ChainMore operator to be added to a merchant as a member, or sign in with a different user that has an active merchant membership. This is a boundary refusal, the JWT itself is correct; the authorization binding is what's missing.
payout_targets_dashboard_only
payout targets can only be changed in the dashboard · HTTP 403
Payout targets are where ChainMore pays out. They can be read with an API key, but only a merchant owner or admin can set or disable one, signed in to the dashboard.
What to do: Ask an owner or admin to change the payout target under Payout targets in the dashboard. Retrying the same request with an API key is always refused.
unauthorized
unauthorized · HTTP 401
The Authorization header was missing, malformed, or the bearer token did not resolve to an active API key.
What to do: Pass Authorization: Bearer <api_key> with a live, un-revoked key. Keys are merchant-scoped and mode-scoped; test-mode keys cannot see live-mode resources.
wrong_auth_kind
wrong credential kind for this surface · HTTP 401
The bearer credential is the wrong kind for the route you called. Control-plane routes (under /v1/dashboard, key/endpoint management, account settings) accept only OIDC access tokens minted by the configured Keycloak realm. Machine routes (payment creation, attempt registration, dispatch) accept only server API keys (cm_test_… / cm_live_…). A JWT presented to a machine route, or an API-key presented to a control-plane route, is refused at the dispatcher before the resolver runs, so this code names the credential-shape mismatch, not a malformed token.
What to do: Switch to the credential kind expected by the surface: use an API key with cm_test_… or cm_live_… prefix for server-to-server payment routes; use a Keycloak-issued OIDC access token (carrying aud=chainmore-gateway) for dashboard / control-plane routes. The webhook test endpoint is the one route that accepts either kind.
Lifecycle & State
attempt_binding_mismatch
attempt binding mismatch · HTTP 409
The supplied attempt id does not match the payment/leg the request is targeting.
What to do: Re-fetch the payment's current attempts and re-issue the request against a matching (payment_id, attempt_id) pair.
checkout_link_already_bound
checkout link already bound · HTTP 409
The customer already chose a different payment method on this link and a payment exists on that rail. A link binds exactly once.
What to do: Continue on the checkout the link already points to (GET the link view returns its checkout_token), or create a new link for a different method.
checkout_link_expired
checkout link expired · HTTP 410
The customer did not choose a payment method before the link's expiry. Nothing was created on any rail.
What to do: Create a fresh checkout link and send the customer the new URL.
checkout_link_method_unavailable
payment method not available on this link · HTTP 409
The chosen method is not in the link's available_payment_methods: the currency does not fit the rail (USD for USDT and USDC, EUR for card), the merchant has no settlement account for it, or the rail is not mounted.
What to do: Offer only the methods the link view lists. Merchants: configure the settlement account (USDT default destination, usdc block) or price in the rail's currency.
dispatch_precondition_failed
dispatch precondition failed · HTTP 409
Dispatch refused because a precondition (admission state, relay health) is unmet.
What to do: Read detail for the specific precondition; some failures resolve on their own (relay health recovery) and are retry-safe, others name a structural reason.
endpoint_disabled
endpoint disabled · HTTP 400
The webhook endpoint is disabled; a disabled endpoint cannot receive new deliveries (including test events and replays).
What to do: Re-enable the endpoint via PATCH /v1/webhook_endpoints/{id} with {"enabled": true} before retrying.
endpoint_secret_missing_envelope
endpoint secret unavailable · HTTP 400
The endpoint's signing secret was enrolled before the envelope-encrypted secret rollout and cannot be decrypted by this gateway build.
What to do: Call POST /v1/webhook_endpoints/{id}/rotate-secret once; the rotate writes a fresh envelope-encrypted secret and returns the new plaintext value.
exec_dispatch_decision_not_found
dispatch decision not found · HTTP 409
The dispatch_decision_id supplied on StartAttempt did not resolve to a decision row.
What to do: Re-fetch the decision (GET /v1/dispatch/decisions/{id}) to confirm it exists, or re-call POST /v1/dispatch/decide and use the fresh id.
exec_dispatch_not_allowed
dispatch not allowed · HTTP 409
StartAttempt was refused because the bound dispatch decision is not in an allowed state; the dispatcher's primary refusal is surfaced in detail.
What to do: Read detail for the dispatcher's primary code and consult the dispatcher section of the OpenAPI spec. Some refusals (relay-health, admission) resolve on their own and are retry-safe after re-deciding.
exec_no_dispatch_decision
dispatch decision id missing · HTTP 409
StartAttempt was refused because the request did not include a dispatch_decision_id; the dispatch gate requires every attempt to bind to a decision row.
What to do: Call POST /v1/dispatch/decide for this (payment, route-candidate) pair first, then pass the returned decision id as dispatch_decision_id on StartAttempt.
invalid_state
invalid state · HTTP 409
The resource is not in a state that permits this operation.
What to do: Re-fetch the resource to read its current state, and issue the action that's legal from that state.
invalid_transition
invalid transition · HTTP 409
The state transition implied by this action is not a legal edge in the resource's state machine.
What to do: Consult the state-machine diagram for this resource family and drive only legal transitions.
replay_of_test_event_forbidden
replay of test event forbidden · HTTP 400
Test events cannot be replayed from delivery history; the replay path is reserved for real deliveries.
What to do: Re-trigger a fresh test event with POST /v1/webhook_endpoints/{id}/test, it fires a synthetic payment.settled each time it's called.
routing_precondition_failed
routing precondition failed · HTTP 409
Routing refused because a precondition (pricing freshness, source-balance, path availability) is unmet.
What to do: Read detail for the specific precondition and retry once it's resolved (for example, re-fetch pricing and re-submit).
session_expired
checkout session expired · HTTP 400
The hosted checkout session has passed its expiry timestamp and can no longer be progressed.
What to do: Create a fresh checkout session and redirect the payer to the new URL.
start_attempt_precondition_failed
start-attempt precondition failed · HTTP 409
A precondition of start-attempt (e.g. pricing freshness, routing validation) is unmet.
What to do: Re-fetch the payment to see the current precondition and retry once the precondition is satisfied.
usdt0_source_unobserved
transaction not visible yet · HTTP 409 · safe to retry
The customer's wallet reported a USDT transaction hash that the source-chain observer cannot see yet. Nothing has been recorded for it.
What to do: Retry the same hash in a few seconds. The hosted checkout does this automatically until the transaction is visible or the reserved step expires; a hash that never becomes visible binds nothing.
validation_precondition_failed
validation precondition failed · HTTP 409
Pre-flight validation refused because a precondition on the payment's current state is unmet.
What to do: Read detail for the specific precondition; most commonly re-fetch the payment and re-issue once it has advanced to a compatible state.
Server-Side Failure
checkout_operation_failed
checkout operation failed · HTTP 500
A checkout-service operation (init-session, bind-wallet, submit, cancel, get-state, widget-session) hit an unexpected server-side failure that did not match any of the typed checkout sentinels.
What to do: Retry after a short delay. If the failure persists, contact support with the correlation_id and the specific operation path.
create_endpoint_failed
create-endpoint failed · HTTP 500
The create-endpoint write hit an unexpected server-side failure; no row was persisted.
What to do: Retry; the operation is safe to repeat. If it keeps failing, contact support with the correlation_id.
create_key_failed
create-key failed · HTTP 500
The create-key write hit an unexpected server-side failure; no row was persisted.
What to do: Retry; the operation is safe to repeat. If it keeps failing, contact support with the correlation_id.
dashboard_operation_failed
dashboard operation failed · HTTP 500
A dashboard read-model operation hit an unexpected server-side failure.
What to do: Retry; reads are idempotent. If the failure persists, contact support with the correlation_id and the dashboard route that failed.
disable_endpoint_failed
disable-endpoint failed · HTTP 500
The disable-endpoint write hit an unexpected server-side failure.
What to do: Retry; disable is idempotent. If it keeps failing, contact support with the correlation_id.
dispatch_failed
dispatch failed · HTTP 500
Dispatch stage returned an error on this payment leg.
What to do: Safe to retry if retryable: true. If the failure persists, the payment will surface the specific dispatch rejection in its dashboard view.
dispatch_lookup_failed
dispatch lookup failed · HTTP 500
Dispatch read-side lookup hit an unexpected server-side failure.
What to do: Retry; reads are idempotent.
handouts_paused
new payment steps are paused · HTTP 503
ChainMore paused handing out new payment steps. Payments already under way are still observed and completed. Nothing was handed out for signing.
What to do: Retry later. A payment already sent from the wallet needs no action; its status updates on its own.
list_endpoints_failed
list-endpoints failed · HTTP 500
The list-endpoints read hit an unexpected server-side failure.
What to do: Retry; reads are idempotent. If it keeps failing, contact support with the correlation_id.
list_keys_failed
list-keys failed · HTTP 500
The list-keys read hit an unexpected server-side failure.
What to do: Retry; reads are idempotent. If it keeps failing, contact support with the correlation_id.
not_implemented
not implemented · HTTP 501
This route is reserved in the OpenAPI spec but not yet wired on this gateway build.
What to do: Check the ChainMore changelog or the OpenAPI spec's x-status extension to see when the route ships.
payment_lookup_failed
payment lookup failed · HTTP 500
Payment read-side lookup hit an unexpected server-side failure.
What to do: Retry; reads are idempotent.
pending_confirmation
the payment state is not yet confirmed · HTTP 503
The data this step rests on is not yet confirmed durable on ChainMore's side. Nothing was handed out for signing and nothing was lost.
What to do: Read the payment or session status after a short wait. Do not repeat the request; the status says what to do next.
rail_not_ready
the payment rail on this network is not ready · HTTP 503
Read with the wallet, before any paid action: the adapter on the chosen network is paused, trusts another authorizer, enforces another fee destination, or already settled this order. Nothing was planned or signed.
What to do: Nothing for the customer to do. The operator fixes the adapter or the runtime configuration named in the detail.
register_direct_tx_failed
register-direct-tx failed · HTTP 500
Registering a direct (non-HTLC) transaction hit an unexpected server-side failure.
What to do: Read detail for the specific refusal; retry-safe if retryable: true.
replay_delivery_failed
replay-delivery failed · HTTP 500
The replay-delivery write hit an unexpected server-side failure.
What to do: Retry after a short delay; if the failure persists, contact support with the correlation_id.
revoke_key_failed
revoke-key failed · HTTP 500
The revoke-key write hit an unexpected server-side failure.
What to do: Retry; revoke is idempotent. If it keeps failing, contact support with the correlation_id.
rotate_endpoint_secret_failed
rotate-secret failed · HTTP 500
The rotate-secret operation hit an unexpected server-side failure; the endpoint row is guaranteed to be unchanged because the mint runs before any UPDATE.
What to do: Retry; the operation is safe to repeat. If it keeps failing, contact support with the correlation_id.
rotate_key_failed
rotate-key failed · HTTP 500
The rotate-key write hit an unexpected server-side failure; the mint runs before any UPDATE, so the key row is guaranteed unchanged.
What to do: Retry; the operation is safe to repeat. If it keeps failing, contact support with the correlation_id.
routing_lookup_failed
routing lookup failed · HTTP 500
Routing read-side lookup hit an unexpected server-side failure.
What to do: Retry; reads are idempotent.
routing_plan_failed
routing plan failed · HTTP 500
Routing could not build a plan for this payment.
What to do: Inspect detail for the structural reason (e.g. no path to canonical asset, pricing stale). Some failures become transient-retry-safe; others require a different payment shape.
server_misconfigured
server misconfigured · HTTP 500
A gateway wiring precondition failed, the request reached a handler that expected middleware state that isn't set.
What to do: This is a server-side bug, not a client bug; retry the request and, if it keeps failing, contact support with the correlation_id.
start_attempt_failed
start-attempt failed · HTTP 500
The execution service rejected start-attempt for a server-side reason (routing, dispatch, or relay).
What to do: Inspect detail for the specific subsystem that refused; retry-safe requests carry retryable: true.
test_event_failed
test-event send failed · HTTP 500
The test-event enqueue hit an unexpected server-side failure (encrypt, pg, or an untagged error from the store).
What to do: Retry after a short delay; if the failure persists, contact support with the correlation_id.
update_endpoint_failed
update-endpoint failed · HTTP 500
The update-endpoint write hit an unexpected server-side failure.
What to do: Retry; the operation is idempotent. If it keeps failing, contact support with the correlation_id.
usdt0_upstream_unavailable
chain endpoint unavailable · HTTP 503 · safe to retry
An Ethereum or Arbitrum RPC endpoint did not answer a block-head, base-fee or log lookup while preparing or advancing a USDT0 step. Nothing was planned, bound or recorded.
What to do: Retry the same call in a few seconds. The hosted checkout retries on its own; a persistent refusal means the configured endpoint is down and the operator has to switch providers.
validation_failed
validation failed · HTTP 500
Pre-flight validation rejected the payment for a server-side reason.
What to do: Read detail for the specific refusal; some failures are transient and retry-safe, others name a structural issue with the payment shape.
validation_lookup_failed
validation lookup failed · HTTP 500
Validation read-side lookup hit an unexpected server-side failure.
What to do: Retry; reads are idempotent.
Validation
checkout_terms_changed
the accepted payment terms no longer hold · HTTP 409
The customer accepted a quoted route on a checkout link; when the payment was planned again the fee, amount, financing, wallet actions or fee bounds no longer matched the accepted terms beyond their headroom. Nothing was signed on stale terms.
What to do: Start a new checkout from the merchant so the route is quoted and reviewed again.
create_payment_failed
create-payment failed · HTTP 400
Create-payment was rejected at the execution boundary.
What to do: Read detail for the specific rejection reason (e.g. unsupported case, missing canonical asset) and re-send with the correction.
dashboard_mode_changed
dashboard mode changed · HTTP 409
The dashboard sent this request for test or live, but the account now uses the other mode. Nothing was changed.
What to do: Load the current mode from GET /v1/dashboard/mode-selection, reload what the dashboard shows, and start the action again.
dashboard_mode_required
dashboard mode required · HTTP 428
A dashboard change must name the mode it was made in (test or live) in the X-ChainMore-Dashboard-Mode header. Nothing was changed.
What to do: Reload the dashboard so it sends the header, then start the action again.
delivery_endpoint_mismatch
delivery belongs to a different endpoint · HTTP 400
The delivery id in the URL does not belong to the endpoint id in the URL.
What to do: List deliveries under the endpoint you intend to replay against (GET /v1/webhook_endpoints/{id}/deliveries) and re-issue the replay with a matching (endpoint_id, delivery_id) pair.
exec_dispatch_binding_mismatch
dispatch binding mismatch · HTTP 409
StartAttempt's (payment_id, route_candidate_id, route_validation_id) tuple does not match the decision row identified by dispatch_decision_id.
What to do: Ensure the StartAttempt binding ids come from the same Plan → Validate → Decide chain; do not mix ids across independent runs.
idempotency_required
idempotency key required · HTTP 400
This write endpoint requires an Idempotency-Key header so that duplicate deliveries do not create duplicate resources.
What to do: Generate a UUID per logical request, send it as Idempotency-Key: <uuid>, and reuse the same value for any retry of that logical request.
invalid_authorization_mode
invalid authorization mode · HTTP 400
The authorization_mode on this request is not one of the values supported by the execution surface.
What to do: Use one of the documented authorization modes (e.g. direct, htlc); see the CreatePayment section of the OpenAPI spec.
invalid_json
invalid JSON body · HTTP 400
The request body was not parseable as JSON.
What to do: Re-send with a well-formed JSON body; ensure Content-Type: application/json and no trailing commas or unquoted keys.
invalid_request
invalid request · HTTP 400
The request was rejected because a required field was missing, malformed, or out of range.
What to do: Read detail for the specific field that failed validation, and re-send with the corrected body.
merchant_limit_reached
the payment exceeds the merchant's limits · HTTP 409
The payment would exceed a limit the merchant has, or the merchant is not yet set up with the limits live payments need. Nothing was handed out for signing.
What to do: The merchant contacts ChainMore to review its limits. The customer may pay a smaller amount or later.
payout_target_not_declared
payout address is not a declared payout target · HTTP 400
The request names a payout address that is not an active payout target of your account for this mode, token and chain. Requests made with an API key may only pay out to declared payout targets.
What to do: Leave the address out to use your declared payout target, or ask an owner or admin to add this address under Payout targets in the dashboard. The same request is refused again until the target is declared.
wallet_account_unsupported
this wallet account cannot pay on this network · HTTP 400
The connected account has code on this network (a contract account or an EIP-7702 delegated account) and the payment path there cannot take the account's own authorization. Nothing was planned or signed.
What to do: Pay from this account on a network where it is a plain account, or from a plain account on this network.
wallet_call_would_revert
the payment call would fail on the network · HTTP 409
The exact payment call was simulated from the wallet before it was handed out and reverts on the network. The detail names the contract's reason. Nothing was signed.
What to do: Prepare the payment again; if it repeats, the reason in the detail tells the operator what to fix.
wallet_chain_not_accepted
merchant does not accept USDT from this network · HTTP 400
Every USDT payout target the merchant declared is on a network the connected wallet cannot pay on (a Tron wallet for an EVM-only merchant, or the reverse). Nothing was planned or signed. The detail lists the networks the merchant accepts, comma-separated.
What to do: Pay from a wallet on one of the listed networks, or ask the merchant to declare a payout target on yours.
wallet_insufficient_native_gas
not enough gas in wallet · HTTP 400
The connected wallet doesn't have enough native token (ETH, MATIC, etc.) to pay the source-chain transaction fee. The fee is small (typically under $1) but must be in the chain's native token.
What to do: Top up a small amount of the source chain's native token, or use a wallet with built-in gas-sponsorship.
wallet_insufficient_usdc
not enough USDC in wallet · HTTP 400
The connected wallet's USDC balance is below the requested amount on the source chain.
What to do: Top up USDC in your wallet, via an exchange withdrawal, a bridge from another chain, or an on-ramp partner like MoonPay or Coinbase.
wallet_insufficient_usdt
not enough USDT in wallet · HTTP 400
The connected wallet holds less USDT than the payment amount at the quoted block. Nothing was planned or signed.
What to do: Top up USDT in the wallet or pay from a wallet that holds the amount, then prepare the payment again.
wallet_not_connected
wallet not connected · HTTP 400
No wallet is connected to the Hosted Checkout. The customer needs to click Connect Wallet before we can determine compatibility.
What to do: Click the Connect Wallet button. ChainMore supports MetaMask, Coinbase Wallet, Rainbow, and any WalletConnect v2-compatible wallet.
wallet_unsupported
wallet not recommended for this payment · HTTP 400
The connected wallet is recognized but not in our supported-wallets list for this payment type. It may work but we cannot guarantee a clean experience.
What to do: We recommend MetaMask, Coinbase Wallet, or Rainbow for this payment. You can continue with your current wallet, we'll do our best, or switch to a recommended one for the cleanest experience.
wallet_wrong_chain
wallet on wrong chain, we'll switch for you · HTTP 400 · safe to retry
The connected wallet is on a chain that doesn't match this payment intent's source chain. Most wallets support a one-click chain switch.
What to do: Click Continue and approve the network-switch prompt in your wallet. Once switched, we'll proceed with the payment.
New codes can be added at any time. Treat an unknown code as a general error, not as a broken response.