ChainMore

Error codes

Every error from the ChainMore API has a stable code. Match on the code, not on the text. Keep the correlation_id and send it to support@chainmore.io if you need help.

{
  "title": "endpoint disabled",
  "status": 400,
  "detail": "this webhook endpoint is disabled; enable it before sending a test event",
  "code": "endpoint_disabled",
  "docs_url": "https://chainmore.io/errors/endpoint-disabled",
  "correlation_id": "corr_4f1c9a0b2d7e6f38a1b2c3d4"
}

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.

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.

HTTP Method

method_not_allowed

method not allowed · HTTP 405

The route exists but the HTTP method you used is not one of its supported verbs.

What to do: Inspect the Allow response header for the list of accepted methods on this path.

Not Found

not_found

resource not found · HTTP 404

The resource identified by the URL does not exist, or is not visible to your API key.

What to do: Re-check the id in the URL. If you're using test-mode keys, test-mode and live-mode resources are strictly isolated; a resource created in one mode is invisible to the other.

Rate Limit

rate_limited

rate limited · HTTP 429 · safe to retry

You have exceeded the per-API-key request budget for this endpoint.

What to do: Back off using exponential jitter and retry. The Retry-After header (when present) names the earliest safe retry time in seconds.

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.