cryptoinfraDocs

#Errors

Every error, on every endpoint, is the same JSON shape. There is no second format to handle.

Open as MarkdownFor AI assistants

#The envelope

json
{
  "error": "insufficient_balance",
  "message": "optional human-readable detail",
  "details": [{ "path": "amount", "message": "Invalid" }]
}
FieldTypeNotes
errorstringAlways present. A stable, machine-readable code. Branch on this.
messagestringOptional prose for a human. May change without notice — never parse it.
detailsarrayPresent on validation_error: one { path, message } per failing field.

error is the contract. message is a courtesy. Code that switches on message text will break the first time someone improves the wording.

#What each status class means for a retry

The status code answers one question — should I send this again? — and the answer is different for each class.

400 — the request is wrong. Body or query failed validation, or a domain rule was violated: validation_error, unknown_asset, asset_disabled, invalid_amount, bad_amount, invalid_address, invalid_pair, no_route; on withdrawals also amount_below_minimum (a zero or negative amount — payouts have no other floor), self_transfer (the recipient is the user's own deposit address — nothing would move but the network fee) and invalid_request. Never retry unchanged. Nothing was created and nothing was charged. Fix the request, or surface the problem to whoever typed it. On validation_error, details[] names the exact field.

401 unauthorized — the key is missing, malformed or unknown. Never retry; no amount of backoff produces a valid key. See Authentication.

403 — the key is real, but your account, your login or the feature may not do this. email_unverified (the owner login has not verified its email), account_paused (an operator hold) and account_suspended answer every call on the key until the state changes — see Authentication. session_required and forbidden_role mean the action exists only on the dashboard, with a second factor and the right role; own_account_only is a dashboard session trying to move a customer's balance, which only your integration may do. None of these is a bug in your request, and retrying does not help; the change that lifts them is ours, and the dashboard shows the state.

400 unknown_asset — an asset id we do not support. Take ids from GET /v1/assets.

404 — not found, or not yours. An unknown id and another merchant's id are deliberately indistinguishable, so you cannot enumerate anyone else's records. Also returned by the idempotency-key lookups when no record was created for that key — which is a meaningful answer there, not an error. See Idempotency.

409 — a conflict with existing state. The request was well-formed but the world disagreed: insufficient_balance, idempotency_conflict, card_state, kyc_not_approved, provider_rejected, not_cancellable, too_many_endpoints, no_own_account (your own balance has not been opened yet) and not_pending (a parked payout someone already decided). Do not blind-retry. Read the current state, decide, and act — most of these change only when something else changes first (a deposit lands, KYC is approved, a card is unfrozen).

Two card errors are retired (20 September 2026) and will never be raised again: 409 card_close_underfunded (a card had to hold the issuer's cancellation charge before it could be closed) and 409 card_refund_wallet_empty (a close waited on the wallet that pays refunds). A close is now always accepted, and where the money is reads as close_refund.state on GET /v1/cards/{id}. Delete any handling of the two codes.

409 provider_rejected is worth calling out: it is a definitive failure. The provider looked and refused, and any charge is already reversed. details.order_id names the order.

500 internal_error — something broke on our side. The body carries no message. Safe to retry with backoff and the same idempotency key; whether the operation landed is indeterminate, so resolve it with the key lookup rather than assuming either way.

503 — a dependency is unavailable, and your request was fine. chain_unavailable (no adapter configured for that asset's chain), cards_disabled, swap_disabled, swap_unavailable, exchange_error. Retry later, unchanged. Do not adjust the request to work around it, and do not queue a modified version of it on your side.

#Retry decision table

StatusRetry?With the same idempotency key?
400, 401, 403No—
404No (it is an answer)—
409 idempotency_conflictNo — fix key generation—
409 othersOnly after state changesYes
429Yes, after backing offYes
500, 502, 504Yes, with backoffYes — always
503Yes, later, unchangedYes

The one rule that never bends: when you retry a mutation, reuse the original idempotency_key. A fresh key on a retry is how a customer gets paid twice.

The same instinct applies everywhere: before you refund, re-charge, or tell a customer something final, reconcile against the resource read. Webhooks are hints; the poll is truth.

#Catalogue

Generated from the components.responses block of the OpenAPI contract (v1.0.0) — this is every named error response and the error codes it can carry.

ResponseHTTPerror codesWhat it means
Unauthorized401unauthorizedMissing, malformed or unknown API key.
AccountNotLive—email_unverified, account_paused, account_suspendedThe key is valid but the account is not live, so every request answers this until it is.
ValidationError400validation_error, unknown_asset, asset_disabled, invalid_amount, amount_below_minimum, bad_amount, invalid_address, invalid_requestBody/params failed validation, or a domain rule was violated.
UserNotFound404user_not_foundUnknown externaluserid for this tenant.
NotFound404not_foundRecord not found (or belongs to another tenant).
InsufficientBalance—insufficient_balanceThe hold would overdraw the user's balance.
ChainUnavailable503chain_unavailableNo chain adapter configured for the asset's chain.
CardBadRequest400validation_error, invalid_request, invalid_amount, card_core_required, card_provider_required, unknown_provider, not_supported_by_providerRequest validation failed or a domain rule was violated.
CardHolderNotFound404holder_not_found, user_not_foundNo matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant.
CardNotFound404card_not_found, user_not_foundUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
CardConflict409kyc_not_approved, card_state, card_issuing, provider_rejected, email_taken, idempotency_conflict, insufficient_balance, no_own_accountA conflicting card state.
CardUnavailable503provider_unavailable, card_rail_unavailable, cards_disabled, rail_unfunded, answeredTemporarily unable to serve this card call — your request was valid.
ExchangeBadRequest400validation_error, invalid_amount, invalid_pair, no_routeThe request was refused before anything was charged.
SwapBadRequest400validation_error, invalid_amount, invalid_pair, no_routeThe request was refused before anything was charged.
SwapConflict409insufficient_balance, idempotency_conflicterror is one of: insufficientbalance (the sell amount would overdraw the user's balance), idempotencyconflict (this idempotency_key was already used with a DIFFERENT payload — your replay is not the request you think it is).
SwapOrderNotFound404order_not_foundUnknown order, or one belonging to another merchant — the two are indistinguishable by design.
SwapDisabled403swap_disabledSwaps and exchanges are turned off, or the exchange integration is not configured.
ExchangeError502exchange_errorThe exchange failed or was unreachable (your request was valid).
ExchangeUnavailable503swap_unavailableOur side is not ready for this chain.

#Debugging an integration

GET /v1/developer/logs returns the method, route, status and duration of your recent calls, kept for 14 days, with no bodies and no query strings. When a call seems to have vanished, check there first: it tells you whether the request reached the platform at all, and what status it left with.

#Daily limits

Calls that cost a third party money per request are capped per account per day: starting a new identity verification (POST /v1/kyc/start, resuming one is free) and uploading card KYC documents (POST /v1/cards/kyc/upload). Past the cap the answer is 429 daily_limit_reached until the next UTC day. If your volume genuinely needs more, ask us to raise it.