Skip to main content

Errors

The envelope​

Every error the API produces itself is JSON in one shape:

JSON
{
"success": false,
"error": "payee_name_mismatch",
"message": "The receiving bank says this account is held by Kettlewick Trading Ltd. …",
"details": {"outcome": "close_match", "…": "…"}
}
FieldMeaning
successAlways false.
errorThe code. Branch on this — together with the HTTP status. Codes are stable.
messageFor people: a log line, or text to show. It changes; never parse it.
detailsOnly on some codes, listed below, where it carries state you are meant to read — the scope you lack, what would clear a payee mismatch. In the sandbox, a 5xx also carries a diagnostic string here; production omits it.

Successful responses are the resource itself — there is no {"success": true, "data": …} wrapper.

Four codes always carry a fixed, generic message, in both environments: internal_error ("An internal error occurred"), database_error ("A database error occurred"), service_unavailable ("Upstream service is unavailable") and gateway_timeout ("Upstream service timed out"). The sandbox puts the specific reason in details; production does not. Every other code, 5xx ones such as platform_paused included, keeps its own message. The detail is in our logs under the response's X-Request-Id — quote that id to support.

Not every error is this envelope​

Some failures happen before our code runs, and those are not JSON:

StatusBodyCauseFix
400Plain text: "Failed to parse the request body as JSON: …"The body is not valid JSONSend valid JSON
415Plain text: "Expected request with Content-Type: application/json"A body without that headerSet Content-Type: application/json
422Plain text: "Failed to deserialize the JSON body into the target type: missing field beneficiary_id …"A required field is missing, or a field has the wrong type — for example an amount that is not a decimalThe text names the field
400Plain text: "Invalid URL: Cannot parse payout_id …" or "Failed to deserialize query string: …"A path or query parameter of the wrong typeCheck the parameter
404EmptyNo such routeCheck the method and path
405EmptyThe path exists, not with this methodCheck the method

So: read the status first; parse JSON only when the Content-Type is application/json. Note that a 422 can be either this plain-text rejection or one of our JSON codes (payee_name_mismatch, plan_limit_exceeded, …).

What to retry​

ResponseRetry?
5xx, 429, a timeout, a dropped connectionYes — with backoff, and with the same Idempotency-Key. For a payout or a conversion, the retry never sends it twice — and never re-sends one the first attempt recorded: it returns that attempt. Read what comes back (Payouts, FX).
409 conflict on fx/execute, after a timeoutNo — the first attempt was recorded; the message carries its id and status (FX)
409 idempotency_key_reuseNo — your key generation is wrong
Any other 4xxNo — fix the request first

Not every route that takes an Idempotency-Key honours it — in production a retried POST /v1/banking/beneficiaries can register a duplicate. See Idempotency for what each route does with the key.


Codes that any route can return​

StatuserrorCauseWhat to do
400bad_requestA validation failure. message says which.Fix the request.
401unauthorizedAny authentication failure — see below.Check the key and the environment.
403insufficient_scopeA restricted key without the route's scope. details.required_scope, details.granted_scopes.Create a key with the scope — Authentication.
403key_not_admittedA key type the route does not admit. details.admits lists what is. Also a 401 when the money or dashboard surface refuses the key type at the door.Use the credential the route admits.
403route_not_available_to_api_keysA route no API key may call.Use a dashboard session.
403permission_deniedA dashboard session whose seat lacks the action's permission (payouts.create, fx.execute, collect.manage, collect.links.create, api_keys.manage). message names it.Ask an owner or admin — seat permissions.
403account_not_verifiedAn account that is not verified, on every route that moves money or needs the bank relationship. Refused before anything is sent to the bank. details: account_id, kyb_status, next_action (complete_verification, await_review, contact_support), delegated, referral, doc_url.Complete verification; the same request works once the account is active.
403api_not_in_planProduction only: your plan does not include this use of the API — live keys and webhooks from Starter, moving money (POST /v1/banking/payouts, POST /v1/banking/fx/execute) from Professional. details.upgrade names the plan that does.See Plan limits and upgrades.
403forbiddenA public session token.See below.
403RESOURCE_FORBIDDENAnother account's resource on a dashboard route — GET /v1/accounts/{id}, GET /v1/accounts?managed_by=, GET /v1/customers/{id}, DELETE /v1/platform-permissions/{id}, POST /v1/accounts?acting_as= — or ?account_id= naming an account that is not yours. Upper case, unlike every other code.Use your own account's ids. ?account_id= is for CashXChain staff.
404not_foundOn the money and developer routes: the resource does not exist or is not yours — they do not tell the two apart.Check the id and the credential.
409conflictThe request conflicts with current state. message says how.Read the state, then decide.
413payload_too_largeA request body over 2 MB, sent with an API key.Send less.
429rate_limitedOur rate limit, or our banking partner's.Wait Retry-After if present, else back off.
500internal_error, database_error, internal_panicOur fault.Retry with backoff; if it persists, send us the X-Request-Id.
503service_unavailableOur banking partner, or a service we depend on, is unavailable.Retry with backoff.
503verification_status_unavailableThe account's verification status could not be read, so a money request was refused rather than risk moving money for an unverified account.Retry with backoff; it is temporary.
504gateway_timeoutOur banking partner did not answer in time. The operation may or may not have happened.Never retry with a new key. For a payout, see Payouts.

Plan limits and upgrades​

What your plan includes is enforced in production; the sandbox leaves every API feature open on every plan, so an integration can be built before it is paid for. A refusal because of the plan is not a dead end: its details.upgrade names the cheapest plan that allows the request.

JSON
{
"success": false,
"error": "api_not_in_plan",
"message": "moving money through the API is not part of the starter plan",
"details": {
"upgrade": {
"current_plan": "starter",
"plan": "professional",
"reason": "live_api_move_money",
"self_serve": true
}
}
}
FieldMeaning
current_planYour plan now.
planThe cheapest plan that allows the request; null when none sold does (talk to us).
reasonWhat was asked for — live_api_read, live_api_move_money, webhooks, currencies, named_accounts, a limit name.
self_servetrue when the plan can be bought in the dashboard; false for Enterprise.
current_limit, plan_limitOn limit refusals: the limit now and under plan.
StatuserrorCauseWhat to do
403api_not_in_planA live key, a webhook, or a payout / FX execution by API, on a plan without it. Checked on every request, so a plan change applies at once.Upgrade, or use the sandbox. Do not retry.
403currency_limit_reachedYour plan's currency limit is used.Close an account, or upgrade.
403named_account_limit_reachedYour plan's local-account limit is used.As above.
422plan_limit_exceededA payout over the plan's single, daily or monthly limit.Wait for the window, send less, or upgrade.

Plans and what each includes: Pricing.

Authentication and access​

All authentication failures are 401 unauthorized; the message tells them apart. The full table is in Authentication.

StatuserrorCauseWhat to do
401unauthorizedNo Authorization: Bearer headerSend one
401unauthorizedUnknown, revoked or other-environment keyCheck CXC_BASE and the key belong together
401unauthorizedAny key on /v1/sandbox/*These routes take a dashboard session
401unauthorizedpk_ on a non-GET requestPublishable keys are read-only
401key_not_admittedpk_ on /v1/banking/*; any sk_, rk_ or pk_ key on a dashboard route. message names what the route admitsUse a key type the route admits
403insufficient_scope"this key does not carry fx:read, which GET /v1/fx/rates requires" — a restricted key without the scope. details.required_scope, details.granted_scopesCreate a key with the scope
403key_not_admittedAny API key on /v1/api-keys, POST /v1/customers or another people-only route; rk_/pk_ on request logs or usage; pk_ on a route whose scope is not publishable. details.admitsUse a session, or the credential the route needs
403permission_deniedA session whose seat lacks the permission: a viewer sending a payout, a member minting a keyAsk an owner or admin
403forbidden"this endpoint is not available to public-API tokens"Use a key, or a dashboard session
403account_not_verified"This account cannot move money until its business verification is approved. Nothing was sent to the bank." — every money route, with one bodyComplete verification — details below

account_not_verified​

The account the request acts for has not been approved by business verification (KYB), or its verification is suspended. Every route that moves money or needs the bank relationship — payouts, FX quotes and execution, beneficiaries, opening or closing accounts, digital-asset transfers — refuses it before anything is sent to the bank. On a call made for a managed account, it is the managed account that is not verified.

JSON
{
"success": false,
"error": "account_not_verified",
"message": "This account cannot move money until its business verification is approved. Nothing was sent to the bank.",
"details": {
"account_id": "9f2b7c10-5d6e-4b1a-9c3f-2e8d7a6b5c41",
"kyb_status": "pending",
"next_action": "await_review",
"delegated": false,
"referral": {
"external_id": "corp_1826115186",
"status": "pending",
"check": "GET /v1/banking/kyb/referrals/corp_1826115186"
},
"doc_url": "https://docs.cashxchain.com/docs/api/errors#account_not_verified"
}
}

Do not retry it in a loop: approval is a person's decision and takes days. Follow details.next_action — complete_verification (no application yet), await_review (submitted; the bank decides) or contact_support (suspended). Nothing was recorded, so once GET /v1/accounts/me reports can_move_money: true you can send the identical request again, with the same Idempotency-Key.

In the sandbox, send Cxc-Simulate: account_not_verified (or account_not_verified; kyb_status=suspended) to receive this response on demand:

Bash
curl -X POST https://api.sandbox.cashxchain.com/v1/banking/fx/quote \
-H "Authorization: Bearer $CXC_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Cxc-Simulate: account_not_verified" \
-d '{}'

Idempotency​

StatuserrorCauseWhat to do
400bad_requestIdempotency-Key missing, empty, over 255 characters, or not visible ASCIISend a valid key
409idempotency_key_reuseThe key was used for a different request — a different body, or a payout key reused for FXUse a new key for a new operation; keep the old one for retries of the old one

Lockdown and holds​

CashXChain can pause money movement platform-wide, and can put a single account on hold. Both stop these writes and nothing else: POST /v1/banking/payouts, POST /v1/banking/fx/execute, POST /v1/banking/beneficiaries, PUT /v1/banking/settlement/policies, POST /v1/banking/accounts/close, Collect refunds, and approving a digital-asset trusted beneficiary (POST /v1/banking/digital-assets/trusted-beneficiaries; revoking one is never stopped). Reads and quotes keep working.

StatuserrorCauseWhat to do
503platform_pausedMoney movement is paused. message is the operator's notice, or "Money movement is temporarily paused. Please try again shortly."Retry later. GET /v1/platform-status tells you when it lifts.
503platform_status_unavailableWe could not confirm the platform is running normally, so we refuse rather than guessRetry shortly
403account_on_hold"This account is on hold. Contact support."Contact support; retrying will not help
503hold_status_unavailableWe could not check holdsRetry shortly

None of them carries Retry-After. GET /v1/platform-status needs no credential:

JSON
{"paused": false, "since": "2026-09-30T18:45:45.000Z", "confirmed": true, "enforced": true}

paused: true with confirmed: false means the status could not be read — treat it as paused.

Delegation​

Refusals of the Cxc-Account header: delegation_malformed, delegation_conflict, delegation_not_supported, delegation_not_permitted, delegation_no_grant, delegation_scope_denied and delegation_requires_session. Each carries details; the table with causes is in Platforms. A managed account that is not verified is refused like your own, 403 account_not_verified, with details.delegated: true.

Step-up​

Dashboard sessions only — API keys never step up.

StatuserrorCauseWhat to do
401two_factor_requiredNo X-Step-Up-TokenAsk the user to confirm with their second factor
401step_up_expiredThe token is older than five minutesMint a new one
403step_up_invalidWrong signature, purpose, factor or userMint a new one
403step_up_not_linkedThe token is not bound to a paymentMint one bound to this payment
403step_up_binding_mismatchBound to a different amount, currency or recipientMint one for this exact payment
403step_up_already_usedTokens are single-useMint a new one
500step_up_not_configuredOur configurationTell us

Accounts​

StatuserrorCauseWhat to do
400bad_request"XYZ is not a currency we can open accounts in"See offered_currencies in GET /v1/banking/accounts
403currency_limit_reachedYour plan's currency limit is usedClose one, or change plan
403named_account_limit_reachedYour plan's local-account limit is usedAs above
409conflict"you already hold a EUR account" — also the answer to a retried openUse the account you hold
422primary_not_eligibleOnly an active EUR, GBP, USD or CAD account can be primaryPick one of those
422primary_accountYou cannot close the primary accountMake another account primary first
404not_found"no enabled banking rail knows account …" — an account id that is not yours or does not existTake ids from GET /v1/banking/accounts
403account_not_verifiedProduction: your account is not approved yet, so there is nothing at the rail to listComplete verification
503service_unavailableNo rail could list your accountsRetry

Beneficiaries and corridors​

StatuserrorCauseWhat to do
400bad_request"corridor validation failed for EUR/DE: …"; "scheme "iban" requires field "iban""; "unknown account scheme …"; "beneficiary_type must be …"Ask GET /v1/banking/corridors and send what it lists
400bad_requestA partner query or field — the rail is chosen for you and is no longer named; accept-verification without payment_idempotency_keyDrop the field; send the key
404not_found"no beneficiary …" — not yours, or no such idTake ids from GET /v1/banking/beneficiaries
404username_not_foundA cxc_handle that names nobodyCheck the handle
409conflictA cxc_handle payee whose account is not active yet; your own account not yet linked to the railWait for verification
422corridor_unsupportedNobody offers this corridor. details.corridor is {type: "payout", currency, country, rail?}. Returned by GET /v1/banking/corridors, POST /v1/banking/beneficiaries and POST /v1/banking/payouts alike.Choose another currency or destination — retrying will not change it
422payee_name_mismatchOn accept-verification: an invalid destination, which no consent clears. details.resolve_by is beneficiaryCorrect the account details and register a new beneficiary
503service_unavailableThe rail that offers the corridor is downRetry with backoff

Payouts​

StatuserrorCauseWhat to do
400bad_request"amount must be positive"; too many decimal places for the currencyFix the amount
400bad_request"the beneficiary is paid in EUR, not GBP"Send the beneficiary's currency
400bad_request"account … holds GBP, so it cannot pay EUR without an fx_quote_id" (production)Lock a rate — Payouts
400bad_request"this quote sells …", "this quote buys …"The quote must match the source account, the currency and the amount exactly
400bad_requestProduction: "a payout pays an exact amount: request its quote with fixed_side: buy" — the quote fixed the sell side, so the beneficiary would be paid an amount the payout never statedQuote with "fixed_side": "buy" and the payout amount
403api_not_in_planProduction, with an API key: moving money by API is not in your plan (Professional and up)Plan limits and upgrades
400bad_requestProduction: the reference is over 18 characters, an unknown purpose_of_payment, or the rail rejected the request outright. Nothing was sent.Read message; fix and send with a new key
400insufficient_balanceSandbox: the account cannot cover the amount plus the feeAdd test funds, then send with a new key
403account_not_verifiedYour account is not verified; nothing was sentKYB
403permission_deniedA dashboard session whose seat lacks payouts.createAsk an owner or admin
404not_found"no beneficiary …" or "no enabled banking rail knows account …"Check the ids
409idempotency_key_reuseSee Idempotency
409quote_expiredThe fx_quote_id has expiredGet a new quote
409quote_unrecordedWe hold no record of that quoteGet a new quote
422corridor_unsupportedNobody pays this currency to this country. Refused before anything is recorded, so the key is not spent.Choose another corridor — final
422payee_name_mismatchThe receiving bank's name check did not pass. details: outcome, account_holder (close match only), check, partner_detail, resolve_by (accept-verification, or beneficiary for an invalid destination), accept_url (null for invalid)Beneficiaries
422plan_limit_exceededOver your plan's single, daily or monthly limit (in USD)Send less, or change plan
422no_rateSandbox: no rate for the currency pairUse another pair
503limit_unmeasurableThe amount could not be priced in USD to check your limitsRetry shortly
503fee_unpriceableSandbox: the fee could not be pricedRetry shortly
503service_unavailableProduction rail: the account's rail could not take the payout, or the rail failed after we asked it. The message is the same for both.The payout may be recorded as created, and a retry with the same key returns it as it is — it is never re-sent. Follow Payouts before sending again.
409route_changedSent with quote_id: the route the payout would take now is not the one quoted. details.quoted_route, details.current_route. Nothing sentAsk for a new quote
409route_quote_usedThe quote_id was spent on another payoutAsk for a new quote
422route_quote_expiredThe quote_id is past its expires_atAsk for a new quote
422route_quote_mismatchThe body is not the one quoted; details.fields names what differsSend the quoted body, or quote this one

A payout from a digital-asset account (sandbox) has codes of its own — travel_rule_incomplete, trusted_beneficiary_required, wallet_not_open, amount_below_minimum, payout_outcome_unknown and more: see Digital-asset errors.

FX​

StatuserrorCauseWhat to do
400bad_requestQuote: fixed_side not sell/buy; same currency twice; a bad amount. Rates: a malformed code, or more than 30 currenciesFix the request
400account_currency_mismatchExecute: account_id does not hold the quote's sell currencyUse the account in the sell currency
400quote_expiredExecute, sandbox: past expires_atNew quote, new key
400quote_consumedExecute, sandbox: already executedNew quote, new key
400insufficient_balance, invalid_amountExecute, sandboxTop up / fix the quote
400bad_requestExecute, production: the rail rejected the request outright; message has its reason. A conversion it accepts and cannot carry out turns failed later instead.New quote, new key
403account_not_verifiedQuote or execute: your account is not verifiedKYB
403permission_deniedExecute, dashboard session: the seat lacks fx.executeAsk an owner or admin
403insufficient_scopeRates or quote: a restricted key without fx:readAdd the scope
409conflictExecute: the key was used already — the message has the conversion id and its statusRead it; FX
409quote_unrecordedExecute, dashboard session: we hold no record of the quoteNew quote
422corridor_unsupportedQuote: nobody converts this pair, or an unknown currency code. details.corridor is {type: "fx", sell_currency, buy_currency}Use another pair — final
422market_closedOutside the pair's market hoursShow message; try later
503service_unavailableThe rail that offers the pair is downRetry with backoff

KYB​

StatuserrorCauseWhat to do
400bad_requestA missing or invalid field; a person's persons[i].… fieldmessage names it
404not_foundAn external_id that is not yours, or that no rail knowsUse the id from your referral
422kyb_precheck_blockedThe pre-check blocked the application; message lists the rulesNothing to retry — the business cannot be onboarded
503service_unavailableNo rail accepts referralsRetry later

The referral outcomes manual_review (202) and already_referred (200) are not errors — see KYB.

Statements and settlement​

StatuserrorCauseWhat to do
400bad_requestStatement: from after to, to in the future, or a period over 366 daysFix the dates
400bad_requestSettlement policy: a malformed currency, equal source and target, a negative min_amountFix the policy
503service_unavailableStatement, production: the account's balance could not be read from the railRetry

The banking rail​

When our banking partner refuses or fails, its answer reaches you as one of these. A refusal (4xx) carries the partner's wording in message, its name replaced by "banking partner"; a failure (5xx) carries only the generic message.

Partner outcomeYou get
Rejected the request400 bad_request
Not found404 not_found — "partner resource not found"
Conflict409 conflict
Rate-limited us429 rate_limited
Market closed422 market_closed
Down, or rejected our credentials503 service_unavailable
Timed out504 gateway_timeout — the operation may have happened. For a payout or conversion, a retry with the same key returns the recorded attempt and does not send it again — see Payouts.
Unreadable answer500 internal_error

This is why a few codes differ by environment — see Environments.


Developer routes​

Route familyStatuserrorCause
Webhooks400bad_requestURL not https (production only — the sandbox accepts http), too long, with credentials, a private IP; description over 200 bytes; an unknown event (the message lists the catalogue)
Webhooks409conflict"this URL is already registered for the account" — also when the existing one is disabled; delete it first
Webhooks404not_foundNo such endpoint
API keys400bad_requestInvalid type; restricted with no scopes; an unknown scope; a bad name
API keys403permission_deniedA session without api_keys.manage; a secret key minted by a seat without payouts.create and fx.execute; a scope the creator's seat does not hold
API keys403key_not_admittedAny API key — keys are managed by people
API keys404not_foundRevoking an unknown or already revoked key
Request logs404not_foundNo such log entry, or not yours
Contacts422contact_username_requiredA cashxchain_user contact without username
Contacts422too_short, too_long, invalid_characters, edge_separator, repeated_separator, no_alphanumeric, reservedAn invalid username
Contacts404username_not_foundThe username names nobody
Contacts403forbiddenEditing an automatic contact (agg: id)

Sandbox tools​

StatuserrorCause
401unauthorizedAn API key on adopt-corporate — it takes a dashboard session. The faucet and the digital-asset tools (kyb/digital-assets/approve, simulate-deposit, simulate-arrival, simulate-completion) take API keys too
400bad_requestFaucet: an amount that is not a whole number from 1 to 100,000, or an Idempotency-Key that is empty, over 255 characters or not visible ASCII
409conflictFaucet: "you hold no active EUR account — open one first"
409idempotency_key_reuseFaucet: the Idempotency-Key sent again with another currency or amount
422asset_unsupportedFaucet: a digital asset — use simulate-deposit
429rate_limitedMore than 30 requests an hour from your account to one sandbox route

The digital-asset tools' codes are on Digital-asset errors.

Digital assets (sandbox)​

Onboarding, digital-asset accounts, wallets, sends, trusted beneficiaries and the route lock: every code is on Digital-asset errors.

The earlier /v1/casp/* routes were removed and answer 404; their codes (casp_not_configured and the rest) are no longer returned.

Dashboard routes​

These routes take a dashboard session or a master key. Their codes, for master-key callers:

FamilyCodes
Collectstripe_not_configured (503), collect_not_onboarded (409), collect_not_active (409), onboarding_incomplete (409), vba_not_ready (409), already_processed (409), cannot_cancel (409), already_fully_refunded (409), invalid_iban (400), no_connect_account (404), payment_not_found (404), expired (410), not_implemented (501). The merchant routes also take sk_ and rk_ keys with collect:read, collect:write or refunds:write; onboarding needs a seat with collect.manage and the other writes collect.links.create (permission_denied otherwise).
Billingbilling_provider_error (503), stripe_not_configured (503), price_not_in_catalogue (503), no_subscription (404), subscription_not_active (409), no_plan_chosen (409), plan_assigned (409), plan_not_billable (400), already_on_plan (409), upgrade_is_already_immediate (409), card_setup_incomplete (409), no_card_setup_in_progress (409), invalid_interval (400), promotion_code_invalid (422), promotion_code_wrong_currency (422), promotion_code_minimum (422), promotion_code_no_plan (409), promotion_code_after_start (409), early_bird_is_better (409)
Usernamesusername_not_found (404), username_not_offered (422), org_username_locked (403), personal_usernames_unavailable (403), the username validation codes above (422), taken (409), recently_released (409)