Using the APIErrors

Errors

One error envelope everywhere, a coarse kind, a specific code, and a request id for support.

The envelope

Every failure, on every endpoint, returns the same shape:

{
  "error": {
    "kind": "invalid_request_error",
    "code": "endorsement_required",
    "message": "The endo_usd endorsement has not been requested for this customer; request it by PATCHing the customer with endorsements: ["endo_usd"].",
    "param": null,
    "request_id": "req_033xhsr1AZZxC8AOkK7y3W"
  }
}
  • kind is the coarse family. Branch on it for generic handling.
  • code is the specific condition. Branch on it for precise handling. Codes are stable API contract.
  • message is for the developer reading the log. It can change, so never parse it.
  • param names the offending field when one field is to blame.
  • request_id finds the exact request in our logs. Quote it to support. It is also on every response as the request-id header.

A failure behind the API reaches you as one of our codes, never as a third party's error.

Kinds

kindHTTPWhen
authentication_error401Missing, unknown, revoked, or expired key. Wrong host for the key
permission_error403Suspended account, incomplete live onboarding, read-only key, sandbox-only endpoint
invalid_request_error400, 405, 409, 413, 422Validation failures, state conflicts, unsupported corridors, a method or body size the route does not take
idempotency_error409, 422Key reuse or a concurrent duplicate
not_found_error404No such resource in your partner and environment, or no such route
rate_limit_error429Over the request rate. Honor Retry-After
api_error500, 503Our fault. Safe to retry: 5xx responses are never stored for idempotent replay

Retry classes

Every code below carries one of three classes:

  • retryable: transient. Retry the same request, with backoff.
  • actionable: succeeds only after you change something.
  • terminal: a different request is required.

Authentication and access

codeHTTPClassMeaning
missing_api_key401actionableNo Api-Key header
invalid_api_key401terminalUnknown key
api_key_revoked401terminalThe key was revoked
api_key_expired401terminalThe rotation grace window has passed
environment_mismatch401actionablesk_test_ key on the live host, or the reverse. The message names the right base URL
partner_suspended403terminalYour account is suspended
partner_terms_required403actionableLive only. Your executed Partner Terms are not on record yet. We record them; contact us
partner_kyb_required403actionableLive only. Your own business verification is not approved
partner_wallet_keys_required403actionableLive only. Your live wallet keys are not registered and activated
read_only_key403actionableA read-only key attempted a mutation
sandbox_only403terminalA simulation endpoint called on the live host
unsupported_api_version400actionableApi-Version names a version we do not serve. The message lists the supported ones

Requests and idempotency

codeHTTPClassMeaning
missing_idempotency_key400actionableEvery POST /api/* needs the header
idempotency_key_reused422actionableSame key, different request. Mint a fresh key
request_in_progress409retryableThe first attempt with this key is still running
invalid_json422actionableThe body is not valid JSON
invalid_parameter422actionableA body or query field failed validation. param names it. Examples: limit above 100; an unknown field on customer create or update, or on a verification submit ("Extra inputs are not permitted"); a fractional JSON number where an amount or fee belongs ("send amounts as strings"); an amount that is not positive or has more than 18 decimal places; a document file that is not valid base64; an unknown kind in event_kinds; a due-diligence value not in the list for the customer's kind; a controllers entry whose id is not a person on the customer; conflicting fields, such as both amount and target_amount on a quote estimate; an unknown asset, or a destination_address that is not a valid address on the wallet's chain
invalid_filter400actionablevirtual_account_id filters GET /api/deposits only
invalid_cursor400actionableThe pagination cursor is malformed
invalid_amount400actionableA wallet-send amount, or a quote estimate's target_amount, is finer than the asset's decimals
amount_out_of_range400actionableOutside the corridor's min or max
rate_limited429retryableHonor Retry-After, then retry unchanged

Terms and customers

codeHTTPClassMeaning
invalid_signed_tos_id400terminalNo accepted agreement has this signed_tos_id
signed_tos_id_already_used409terminalOne acceptance binds one customer. Mint a new link
tos_already_accepted409terminalThe customer is already on the current terms version
partner_reference_id_exists409actionableAnother object of the same resource (customer, virtual account, or wallet) already uses this reference id in this environment
unknown_endorsement422actionableThe endorsement name is unknown or not enabled. The message lists the available ones
endorsement_profile_required422actionableRequesting endo_usd needs email and phone on an individual, or email on a business, sent in the same request if not already set. The message names the missing ones. Tax data is not needed to request it: the review asks for it through the missing bucket

Verification

codeHTTPClassMeaning
customer_tos_required400actionableThe customer has no terms acceptance on record
endorsement_required400actionableOn a submit, the customer has no endorsement requested. Request one on create or PATCH, then submit
hosted_intake_unavailable400actionableHosted collection is not available yet. The submit named intake_method: "hosted", and this comes ahead of the submit's other refusals in this table. Omit intake_method, or send api, and share the customer's details and documents through the API. redirect_url without intake_method: "hosted" returns 422 invalid_parameter instead
verification_not_needed409terminalEvery requested endorsement is already under review or decided, so the submit has nothing to send
verification_incomplete422actionableSomething the review needs is still owed, such as tax_id, or a document was not accepted because it could not be read, has expired, is too old, carries another name, or failed its checks. The message names each such document and why, then everything else owed, and param names the first. That document's code is in the endorsement's issues bucket: send a new one
document_reading_unavailable503retryableThe customer's documents could not be read right now. Nothing was submitted. Retry the submit
verification_not_started400actionableSandbox only. A simulated decision needs a submitted review, and the customer was never submitted
verification_not_under_review400actionableSandbox only. No review is running to decide, for example while the endorsement is in issues. Answer the ask with a PATCH instead

Virtual accounts

codeHTTPClassMeaning
endorsement_required400actionableOn a virtual-account create, the currency's endorsement was never requested, and the message names the exact request. On a wallet create, the customer has no approved endorsement yet
endorsement_not_approved400actionableThe endorsement was requested but is not approved. The customer's requirement buckets say why
customer_name_required400actionableThe customer needs a name: it becomes the account holder name
unsupported_corridor400actionableThat currency and destination pair is not enabled. Check GET /api/corridors
unsupported_rail400actionableThat rail does not serve that currency
rail_unavailable400actionableA rail named in payment_rails cannot be served right now. Name only available rails, or omit payment_rails
virtual_account_exists409actionableThis customer already has an account in that currency. The message names it. PATCH its destination instead
no_fee_destination400terminalPartner fees need your balance account set up. Contact support

Quotes

codeHTTPClassMeaning
unsupported_corridor400actionableNo enabled corridor delivers that asset on that chain from that source
no_rate_available400retryableNo price for the pair right now
invalid_amount400actionabletarget_amount is finer than the target asset's decimals
amount_out_of_range400actionableOutside the corridor's min or max

Wallets and operations

codeHTTPClassMeaning
wallet_keys_required400actionableThe public halves of your Primary Signing Key and Recovery Key are not registered for this environment. Register them on the dashboard's Wallets page. We never accept a private key
activation_required400actionableYour Primary Signing Key for this environment is registered but not yet activated with your Recovery Key. Activate it on the dashboard's Wallets page
unsupported_chain400actionableThat chain is not enabled
wallet_not_owned400actionableaddress_wallet_id must name a wallet of the same customer
unsupported_address_share400actionableAddress sharing is EVM only, on both sides
address_exists409terminalThat address already has a wallet on that chain
wallet_not_active400actionableThe wallet is frozen or closed
insufficient_balance400actionableThe wallet's on-chain balance of the asset, less what other operations already commit, is below the amount. Committed means executing, or awaiting authorization and not yet expired
operation_expired400actionableThe 30-second window elapsed. Create a new operation
operation_not_authorizable400terminalThe operation already reached a terminal state
invalid_signature422actionableNot your Primary Signing Key's raw 64-byte ed25519 signature over the digest, base64 encoded

Not found

404 not_found_error codes are per resource: customer_not_found, virtual_account_not_found, wallet_not_found, wallet_operation_not_found, tos_link_not_found, webhook_not_found, webhook_event_not_found, deposit_not_found, conversion_not_found, payout_not_found, transaction_not_found. All are terminal.

404s are ownership-scoped. Asking for another partner's customer returns not_found_error, not 403, so resource existence never leaks across partners.

Routing and transport

codeHTTPClassMeaning
route_not_found404terminalNo such route. Check the method's path
method_not_allowed405terminalThe route exists but not for this method. The Allow header lists the methods it takes
request_too_large413actionableThe body is over 128 MiB. Single files are capped near 10 MB and refused as 422 invalid_parameter

Server errors

codeHTTPClassMeaning
internal_error500retryableSomething went wrong on our side
upstream_error503retryableA system behind the API could not complete the request. Nothing was charged or changed
archive_unavailable503retryableA held document could not be read back for submission. Nothing was submitted

Retry a 5xx with the same Idempotency-Key: a 5xx is never stored for replay, so the retry executes fresh.