Digital-asset errors
Every error uses the one envelope:
{"success": false, "error": "<code>", "message": "…", "details": {…}}.
Branch on error and details; show message to a person. Codes that any
route can return — authentication, scopes, rate limits, lockdown — are in the
error reference.
Was anything recorded?
On POST /v1/banking/payouts from a digital-asset account:
- Without
details.idempotency_key_spent: true, nothing was recorded or sent, the step-up was not spent and theIdempotency-Keyis unused. Correct the request and send it again under the same key. - With
details.idempotency_key_spent: true— every error after the send was recorded carries it, a5xxincluded — the key is spent. Its payout,details.payout_id, isfailed— nothing reached the provider, or the provider refused its transfer outright (422 destination_not_payable,provider_refused) — or, for503 payout_outcome_unknown, being confirmed. Read it withGET /v1/banking/payouts/{payout_id}, and use a new key for a new send.
On earlier_payout_unconfirmed, details.payout_id is the earlier payout;
a send recorded and then refused so names its own in
details.recorded_payout_id.
Onboarding and accounts
| Status | error | When |
|---|---|---|
503 | digital_assets_unavailable | No digital-asset rail on this deployment (production today), or its provider offers no sandbox approval or deposits |
409 | digital_assets_onboarding_required | Opening an account, a wallet or sending before your verification with the provider is active — details: {state, next_steps} |
409 | digital_assets_onboarding_incomplete | The provider holds too little about you to open the account or wallet (no address, terms not accepted). Complete its verification and try again |
409 | conflict | Opening an account: "you already hold a USDC account". Sandbox approval: the provider already holds a wallet for a customer from an unfinished hosted verification — finish that verification |
422 | currency_unavailable | Opening an account in an asset not offered here |
403 | account_not_verified | Your business is not verified (KYB). The sandbox lets a pending account through |
403 | currency_limit_reached | Your plan's currency limit is used up. All digital assets together count as one |
422 | account_not_closable | Closing the USDC account |
429 | rate_limited | Sandbox approval: more than 30 calls an hour for your account |
400 | bad_request | Sandbox approval repeated more than 24 hours after an approval that has not completed |
503 | service_unavailable | The provider cannot be reached. Retry with backoff |
Wallets
| Status | error | When |
|---|---|---|
400 | bad_request | No Idempotency-Key |
422 | network_unsupported | No asset is offered on that chain here |
409 | digital_assets_onboarding_required, digital_assets_onboarding_incomplete | As for opening an account |
403 | account_not_verified | As above |
503 | digital_assets_unavailable | No digital-asset rail |
Test funds and other sandbox tools
| Route | Status | error | When |
|---|---|---|---|
simulate-deposit | 400 | bad_request | No Idempotency-Key; an amount not positive, over 100,000 or past six decimal places |
simulate-deposit | 404 | not_found | Not one of your accounts |
simulate-deposit | 409 | conflict | The account is not open |
simulate-deposit | 409 | wallet_not_open | No wallet of yours on the network named, or on any chain carrying the asset |
simulate-deposit | 422 | account_not_digital | A bank account — use the faucet |
simulate-deposit | 422 | asset_not_on_network | The asset is not offered on that chain |
simulate-arrival | 404 | not_found | Not one of your payouts |
simulate-arrival | 422 | not_eligible | details.reason: not_a_send_back, not_accepted, returning, too_old, not_to_a_sandbox_account, no_mock_rail, no_digital_rail |
simulate-arrival | 503 | service_unavailable | The arrival is being booked by another request at that moment; ask again shortly |
simulate-completion | 404 | not_found | Not one of your payouts |
simulate-completion | 409 | not_simulatable | details.reason: debit_not_seen (ask again in a few seconds), not_accepted, needs_reconciliation, provider_state (details.partner_status), wallet_not_open, asset_not_on_network, arrival_not_seen — see Settling a send now |
simulate-completion | 422 | not_eligible | details.reason: not_a_digital_asset_send, completes_at_provider (on chain, across chains, by @handle), no_casp_layer, no_digital_rail, provider_not_sandbox |
simulate-completion | 503 | service_unavailable | Being completed by another request at that moment, the provider could not be read, or its sandbox did not take a swap's bought asset; ask again shortly |
| faucet | 422 | asset_unsupported | A digital asset (USDC, EURC, …): the faucet credits bank accounts only — use simulate-deposit |
| any | 429 | rate_limited | More than 30 requests an hour from your account to that route; the message says how many minutes are left |
| any | 503 | digital_assets_unavailable | No digital-asset rail, or no sandbox deposits at its provider |
Sends
POST /v1/banking/payouts and its quote, from a digital-asset account. "Recorded" means the answer carries details.idempotency_key_spent: true.
| Status | error | When |
|---|---|---|
400 | bad_request | More than one destination; currency not the account's asset; more than two decimal places; no source_network; a chain no send leaves from; a top-level destination_network other than destination.network, or any on a send to an IBAN, a bank account or by handle; a destination.network on a send by handle; a memo on a chain other than Stellar; an address not on the chain; a malformed IBAN, BIC, business identifier or country; a reference the rail does not take, or any reference on chain, by handle or for a swap; a holder's name outside 3–35 characters; a swap into the asset it sells; rail, fx_quote_id or beneficiary_id beside a send; your own handle. Recorded: the rail's own checks refused it, nothing sent |
400 | bad_request (bank accounts) | A routing number, account number, sort code, CLABE or Pix key that fails its rule; a rail the account is not paid over; a US holder without a whole address and state; a us_bank without bank_name (or blank, over 256 characters, a control character), named as destination.bank_name; a trusted us_bank entry approved without one (details.fields, details.trusted_beneficiary_id) |
400 | insufficient_balance | Recorded. The wallet on that chain, less what your other payouts from it hold, does not cover the send and its headroom (1 %; 5 % on Ethereum). Each holds its amount and headroom until its debit is read, from a minute after that only the headroom, and what is left of it for about a minute after it finishes — see Holds. The message names what is free and what the send needs, the amount and the headroom apart: available 9.10 USDC, needed 10.10 USDC: 10.00 USDC for the payout plus 0.10 USDC headroom, held while it is in flight. Nothing sent |
400 | trusted_beneficiary_required | An API key sent destination details, or no trusted_beneficiary_id — see Sending with an API key |
401 | two_factor_required | A session without X-Step-Up-Token |
403 | step_up_binding_mismatch, step_up_not_linked, step_up_invalid, step_up_expired, step_up_already_used | The step-up |
403 | session_required | A session or key acting for another account (Cxc-Account) |
403 | permission_denied | A seat without payouts.create |
403 | insufficient_scope | A restricted key without payments:write |
404 | not_found | An account, trusted beneficiary or handle that is not yours or does not exist |
409 | account_not_active | The account is not open |
409 | wallet_not_open | No wallet of yours on source_network (details.network): open one with POST /v1/banking/digital-assets/wallets |
409 | digital_assets_onboarding_required | Your verification with the provider is not active; or (details.rail) the send is paid over a rail the provider has not enabled for you — a bank account outside SEPA over USD (ACH, wire, FedNow), GBP (Faster Payments), MXN (SPEI) or BRL (Pix), or an IBAN while your EUR (SEPA) rail is not enabled. Nothing recorded, the step-up not spent |
409 | earlier_payout_unconfirmed | An earlier payout of yours on the digital-asset rail — any send, or a send-back — is still being confirmed (details.payout_id), or parked for support (details.resolve_by). Nothing recorded: send this one again, under the same key, once that payout reads submitted or failed, usually within minutes |
409 | idempotency_key_reuse | The key was used for another body |
409 | conflict | The provider already holds a registration of this IBAN or bank account that we have not recorded. Nothing sent; support resolves it |
422 | corridor_unsupported | details.corridor: sends are off here, it is production, or not a route the provider offers — or a bank kind closed to your business's region (type bank) |
422 | asset_not_on_network | The chain does not carry the asset here (a swap: either asset); details.asset, details.network |
422 | amount_below_minimum | Under the provider's minimum (details.minimum, details.currency) — see Minimums |
422 | travel_rule_incomplete | The beneficiary or holder lacks what the travel rule needs; details.fields lists every field |
422 | wallet_attestation_required | Past 1,000 of the asset to your own self-custodied wallets in 30 days; details.asset, threshold, window_days, sent, remaining |
422 | destination_is_own_wallet | The address is one of your own wallets |
422 | recipient_cannot_receive | By handle: they hold no open account in the asset, are not verified, or the asset may not reach their region; details.asset |
422 | digital_send_limit_exceeded | Over a per-asset cap; details.asset, window, cap, used, remaining |
422 | beneficiary_not_trusted | The trusted beneficiary is revoked, cooling_down (an API key), or a handle entry whose handle names someone else (recipient_changed); details.reason |
422 | destination_not_payable (details.reason provider_refused) | Recorded. The provider refused the transfer when it was created, for good, in answer to the only request sent — a Stellar account with no trust line for the asset, a field it will not take, a rail your provider customer is not endorsed for. Only a refusal of what you sent is in the provider's words: the message carries its sentence, and details.provider_field names the field when it named one. Any other refusal is in our words and says to contact support; it carries details.provider_field only for a rail (destination.payment_rail), and you still contact support rather than change that field. Nothing was created and nothing is held: the payout is failed, and the next send is not held behind it. Change the field and send again under a new key — or, for a refusal in our words, contact support first. See A refusal when the transfer is created |
422 | destination_not_payable | Nothing sent; details.reason: not_trusted — the entry was revoked before the send's hold was written (recorded failed); rail_not_supported — the provider reports the bank account cannot be paid over the rail asked; provider_record_mismatch — to an IBAN or bank account: the provider's record of the payee no longer matches it, contact support; not_prepared — to an IBAN or bank account: its registration with the provider was not found when the transfer was about to be made, send it again; own_wallet — the address is one of your own wallets, as the rail found when it sent (otherwise refused earlier as destination_is_own_wallet); recipient_cannot_receive — by handle: the recipient's wallet could not be reached when the rail sent |
500 | internal_error, database_error | Ours. Nothing recorded — or, recorded: nothing sent, closed failed within minutes |
503 | service_unavailable | The provider is down. Nothing recorded: retry with backoff under the same key. Or recorded failed, nothing sent (details.idempotency_key_spent: true) — among them the provider refusing our own credentials when the transfer was created: ours to fix, and the next send is not held behind it. Then retry later under a new key: the same key answers with the failed payout |
503 | payout_outcome_unknown | Recorded. The provider did not confirm the send: no answer, an outage, a rate limit, a conflict, or a refusal that came after a retried attempt (the attempt before it may have created the transfer). Only a refusal of the single request sent is 422 destination_not_payable (provider_refused) above. Send it again only under the same Idempotency-Key, which answers with the payout once it is known: submitted, or failed once the provider has twice found nothing under it (seven minutes or more). Until then your other sends on the digital-asset rail are refused 409 earlier_payout_unconfirmed |
Send-back to your EUR account
| Status | error | When |
|---|---|---|
400 | bad_request | beneficiary_id as well, an fx_quote_id, a rail other than sepa, more than two decimal places, or a destination account that does not hold currency |
400 | bad_request (recorded) | "The payout was not sent: …" — the provider could not be read before the transfer was created. Nothing sent; use a new key |
400 | insufficient_balance | Recorded. The USDC account's balance, less what your other USDC payouts from the same wallet hold — send-backs, sends and swaps — does not cover this one's max_source_amount. A send-back holds its whole max_source_amount until its debit is read, from a minute after that only the part above the debit, and what is left of it for about a minute after it finishes. The message names what is free, what this send-back needs, and that need's two parts: its estimate and the headroom held on top |
401 / 403 | two_factor_required, step_up_* | A session's step-up |
403 | session_required | A session or key acting for another account |
403 | insufficient_scope | A restricted key without payments:write |
404 | not_found | destination_account_id or source_account_id is not one of your accounts |
409 | conflict | The provider already holds a registration of the destination that we have not recorded; support resolves it |
409 | earlier_payout_unconfirmed | Another send-back from the account is still being confirmed (details.payout_id) |
422 | corridor_unsupported | Not offered here: details.corridor {"type": "own_account", "currency", "source_currency"} |
422 | destination_not_payable | Nothing sent; details.reason: same_provider, not_active, no_iban, currency (the account does not hold EUR), holder_not_customer (not held in your company's legal name), sandbox_only; holder_name_length — the account's legal name is outside the provider's 3 to 35 characters, so it cannot be registered: contact support; holder_name_unsplittable — an individual's account whose name has no first and last name: contact support; account_changed — the account's IBAN changed since it was registered: contact support; provider_record_mismatch — the provider's record of the account no longer matches it: contact support; not_prepared — its registration was not found when the transfer was about to be made: send it again |
422 | destination_not_payable (details.reason provider_refused) | Recorded (details.payout_id). The provider refused the transfer when it was created, for good, in answer to the only request sent. Only a refusal of what you sent is in the provider's words: the message carries its sentence, and details.provider_field names the field when it named one. Any other refusal is in our words and says to contact support; it carries details.provider_field only for a rail (destination.payment_rail), and you still contact support rather than change that field. Nothing was created and nothing is held: the payout is failed, and the next send-back is not held behind it |
422 | payee_name_mismatch | The provider's name check says the account is not held in your name; details.resolve_by is support |
422 | plan_limit_exceeded | Over the plan's limit, measured on the EUR |
503 | service_unavailable | Unavailable right now; nothing recorded. Retry with backoff. Recorded (details.idempotency_key_spent, details.payout_id): the provider refused our own credentials, at the transfer's create or before it — the payout is failed, the next send-back is not held behind it; a new key, later |
503 | payout_outcome_unknown | Recorded; the provider did not confirm it. Only the same key |
Convert EUR into USDC
| Status | error | When |
|---|---|---|
400 | bad_request | beneficiary_id, fx_quote_id, a reference, a rail other than sepa; under 1.00 EUR or more than two decimal places |
404 | not_found | Not one of your accounts |
409 | destination_not_ready | details.reason payee_settling or registration_pending; send the same request (same key) again from details.ready_at |
409 | earlier_payout_unconfirmed | An earlier convert from the account is still being confirmed (details.payout_id) |
422 | corridor_unsupported | Not offered: details.corridor {"type": "own_account", …} |
422 | destination_not_payable | details.reason: not_active, no_iban, account_changed, payee_refused, payee_address_unknown (no complete registered address for your company: complete it, or contact support) |
422 | payee_name_mismatch | The bank's name check does not confirm the USDC account under its provider's name; details.resolve_by is support |
422 | plan_limit_exceeded | Over the plan's limit, measured on the EUR |
401 / 403 | two_factor_required, step_up_* | A session's step-up |
503 | service_unavailable | Your bank or the provider is unavailable. Retry with backoff |
504 | gateway_timeout | The provider did not answer in time. Retry |
503 | payout_outcome_unknown | Recorded; your bank did not confirm the payment. Only the same key |
500 | database_error | Recorded, nothing sent; closed failed within minutes |
Trusted beneficiaries
| Status | error | When |
|---|---|---|
400 | bad_request | A malformed field, a field of another kind, a missing network, address or iban, a us_bank without bank_name, a cxc_handle entry for your own handle; no Idempotency-Key |
401 | two_factor_required | No step-up |
403 | session_required | Any API key, master keys included |
403 | delegation_not_supported | A session acting for another account |
403 | permission_denied, step_up_* | A seat without payouts.create; the step-up |
403 | account_not_verified | Your business is not verified |
404 | not_found | Approving a cxc_handle entry for a handle no CashXChain account has; revoking an id that is not yours |
409 | trusted_beneficiary_exists | The destination already has an active entry (details.trusted_beneficiary_id) |
409 | idempotency_key_reuse | The key was used for another body |
422 | travel_rule_incomplete, destination_is_own_wallet, network_unsupported | As for a send |
422 | corridor_unsupported | GBP, MXN or BRL for a business in the EEA — nothing approved |
503 | digital_assets_unavailable | No digital-asset rail |
The route lock
| Status | error | When |
|---|---|---|
400 | bad_request | quote_id is empty |
404 | not_found | No quote of yours has this id |
422 | route_quote_expired | After expires_at (details.quote_id, details.expired_at) |
409 | route_quote_used | The quote was spent on another payout |
422 | route_quote_mismatch | The body is not the one quoted (details.fields) |
409 | route_changed | The route would be another now (details.quoted_route, details.current_route); nothing sent |
Quotes
A quote runs the send's checks short of the travel rule's completeness, the
caps and who is asking, and answers the same codes for them. On a convert's
quote the provider may also refuse: 404 not_found (the account is not open
at the provider), 409 conflict (your verification there is not active),
400 bad_request (below its minimum), 504 gateway_timeout, and
503 service_unavailable when it gives no estimate.