Errors

One envelope, one meaning per status code.
View as MarkdownOpen in Claude

Every failure returns the same shape, with the HTTP status repeated in error as a string:

{
"message": "Unknown assetId: 99",
"error": "400",
"requestId": "b7a1f0c2-3d4e-4a5b-9c6d-7e8f90a1b2c3"
}

message is always safe to show a user and safe to log. Internal detail never appears in it — when something breaks on our side, the specifics go to our audit log against the requestId, and you get a generic message. Quote the requestId when you report a problem and we can pull up the exact request.

Status codes

StatusMeansWhat to do
400Bad input — a body that is not valid JSON, a missing field, a malformed address, an unknown assetId, an unparseable amount, an amount below the minimum, or a missing X-Request-Id on /send.Fix the request. Retrying it unchanged will fail identically.
401Not authenticated, or a wrong password on /send and /extend.Refresh the token, or fix the password. Nothing was signed or broadcast.
402The wallet cannot cover the transfer — not enough of the asset, or no native coin to pay gas.Top the wallet up. Nothing was signed or broadcast.
403Refused on purpose. /extend returns it when the seed does not derive the user’s address; /auth and /refresh return it when your api client is not fully provisioned; any asset-scoped call returns it for an assetId your api client is not enabled for.Do not retry. For an asset, contact your operator to have it enabled.
404No such user — or, on /txid, a transaction hash the chain does not know.For a fresh broadcast, wait and poll again — it may not have propagated yet. For a user, check the userId: retrying unchanged will not help.
409A conflict — a transfer with this X-Request-Id already in flight, a wallet busy with another transfer, a transfer we could not cover for you right now, a wallet that already exists, a seed that cannot be verified against that user, or a trade that is not in a state you can act on.On /send, back off and retry with the same id. Never mint a new one. On a trade, poll /trade — the status tells you why.
422The amount is smaller than the network fee deducted from it (native sends), or the X-Request-Id is already committed to a different transfer.Fix the amount — and since that is a new transfer, give it a new id. Nothing was broadcast.
500Something failed on our side. Transient.Safe to retry a read. Retry a /send with the same X-Request-Id — see below.
502The chain rejected the broadcast (Tron), or an order was rejected and nothing traded.Read the message. Do not retry unchanged.
504A price or an order did not answer in time — on /trade/approve the outcome is unknown.Poll /trade; never approve again. Elsewhere, retry with backoff.
503That asset is not available on your endpoint — or a read (transaction history, trading) is temporarily down.The asset case is permanent: do not retry, contact your operator. The message tells you which: Chain is not configured is permanent, temporarily unavailable is not.

The two that matter

401 on /send means the password was wrong — nothing was broadcast, so fixing it and retrying is completely safe. A 500 or a timeout is ambiguous: the transaction may have been signed and broadcast before the failure reached you. Retry it with the same X-Request-Id — the service will replay the original result rather than send twice. Retrying under a new id is how you send twice. See Reliability.

A 200 from /send is not a confirmation. It returns a txHash as soon as the network accepts the transaction. It can still fail on-chain — out of gas, reverted, dropped. Poll /txid until status is success or failed, and treat pending as unfinished.

Common messages

MessageStatusCause
Request body is not valid JSON.400The body could not be parsed at all, so the endpoint’s own validation never ran.
Request body is too large.400The body is over the size limit. Every request on this API is a small JSON object.
assetId is required400You omitted assetId. Send it explicitly on every call.
X-Request-Id header is required for /send — reuse the same value when retrying a transfer400/send needs a client-supplied idempotency key.
Invalid X-Request-Id — 1-64 characters of A-Za-z0-9._-400The header is present but malformed.
Unknown assetId: 51400Not a supported asset — or one that is not available on your endpoint. The offending id is included in the message. See Asset IDs.
Wallet not found for user on this chain.400That user has no wallet on the asset’s chain — usually because the chain was unavailable when the user was created. Backfill with /extend.
Missing/Invalid userId400userId must be a 24-character hex id.
Invalid amount400amount must be a decimal string like "0.5" — not a float, not base units, not negative.
Amount below minimum of 0.0001ETH400Native EVM sends have a floor of 0.0001 (ETH, BNB, POL).
Minimum send amount is 0.00001 BTC400Bitcoin’s floor.
Amount must be greater than zero.400Token sends cannot be zero.
Amount has more than 6 decimal places400More precision than the asset’s decimals allow.
Missing/Invalid address400A malformed recipient (or sender on /fee).
Sender and recipient must differ400Self-transfers are rejected.
Missing/Invalid password400/send on Bitcoin with no password in the body. A wrong password is 401 Invalid credentials.
Unauthorized401No bearer token, or a malformed Authorization header.
Invalid or expired token401The access token expired or does not verify. Call /refresh.
Missing refresh token401/refresh was called without a refreshToken in the body.
Invalid refresh token401The refresh token expired, was already consumed, or was revoked by a replay. Go back to /auth.
Invalid credentials401Bad api key on /auth, a deactivated api client on /refresh, or the wrong keystore password on /send / /extend (Ethereum, BSC, Polygon, Tron and Solana) — including a /create that resolves to a wallet set the password does not open.
Insufficient native funds.402Not enough of the native coin for the transfer.
Insufficient native for gas.402The token balance is fine, but there is no native coin to pay the gas.
Insufficient token balance402Not enough of the token.
Do not send X-USER-ID to a platform endpoint — it always acts as the platform wallet.400/platform/* endpoints resolve their wallet from your credentials; drop the header.
The platform wallet is not reachable from user endpoints.403The platform wallet is only served under the /platform prefix — use the twin endpoint.
This platform has no wallet.404Your api client has no platform wallet yet — the service operator provisions it.
Seed does not match this user.403The mnemonic sent to /extend does not derive that user’s stored address.
Transaction not found404Unknown hash, or a broadcast that has not propagated yet.
Address not found404On Bitcoin, the address is not one the indexer knows. Check the assetId and the user’s wallet.
Transaction history is temporarily unavailable503History could not be read for Ethereum, BSC or Polygon. Transient — retry with backoff. An empty list now always means “no transactions”, never a failure.
Failed to get BTC balance502The Bitcoin indexer did not answer. Transient — retry with backoff.
Wallet already exists.409A derived address is already registered.
A transfer with this X-Request-Id is already in progress — check /transactions before retrying.409The original attempt has not resolved yet. Back off and retry with the same id.
Another transfer from this wallet is in progress — retry shortly.409On Ethereum, BSC and Polygon, transfers from one wallet are serialised. Retry with the same id after a short backoff.
Transfer is being prepared — retry shortly with the same X-Request-Id.409The transfer is being set up and the outcome is not yet certain. Retry with the same id — never a new one, and never assume nothing went out.
This transfer cannot be covered for you right now — retry shortly with the same X-Request-Id.409On assets where transfers are covered for you, the cover was momentarily unavailable. Retry with the same id after a backoff. If it persists, the wallet can still send by holding the chain’s native coin itself.
This X-Request-Id was already used for a different transfer.422You reused an id with a different assetId, recipient or amount. A new transfer needs a new id.
Amount does not cover the network fee.422On a native send the fee is taken out of the amount — and this amount is smaller than the fee.
Transaction was rejected by the network.502Tron refused the broadcast. Nothing went out.
Provide exactly one of fiatAmount or cryptoAmount400A quote fixes one leg and derives the other — send one, not both or neither.
Amount must be greater than zero400A quote cannot be for nothing.
Quoted amount rounds to zero — increase the amount400The derived leg is smaller than one base unit at the current price.
Unsupported fiat: JPY400Settlement currency must be one of USD, EUR, GBP, CAD, AUD, SEK, DKK, PLN, BRL or AED.
Missing/Invalid tradeId400tradeId must be a 24-character hex id, as returned by /order or /sell.
Trade not found404No trade with that id for that user.
Quote has expired — request a new one.409You approved after quote.expiresAt. The trade is now expired.
This trade is already being filled — poll /trade before retrying.409The order is placed and the outcome is not yet confirmed. Never approve again.
Trade cannot be approved from status: expired409Already expired, failed or cancelled — quote again.
Trade cannot be cancelled from status: approved409Only an open quote can be cancelled.
Trade is no longer open409Another request approved or cancelled it first.
This trade can no longer be settled — request a new quote.409The terms the trade was quoted under no longer apply. Nothing traded — quote again.
Order rejected: the order could not be filled502The order was refused; nothing traded and the trade is now failed. The text after Order rejected: varies.
Pricing is temporarily unavailable — try again shortly.502 / 504No usable price. On /order and /sell nothing was quoted; on /trade/approve see the 504 row above.
Trading is temporarily unavailable503Trading is not available for that asset right now. Transient — retry with backoff; contact your operator if it persists.

Retrying a /send

/send is idempotent on the X-Request-Id header you supply. Retrying a transfer is safe as long as you send the same id — a completed original replays its txHash instead of broadcasting again, and an unresolved one returns 409 rather than double-sending.

The failure mode that remains is entirely on the client side: minting a new id for what is really the same transfer. That is not a retry — it is a second transfer, and the service will carry it out.

Do not decide to re-send by checking /transactions and retrying under a new id if you don’t find the transfer. EVM history lags a fresh broadcast, so a transfer sent seconds ago is routinely absent — and transaction rows carry no counterparty address, so you cannot positively identify one transfer anyway. “Not found” does not mean “not sent.”

Reliability sets out the full retry table and what to do when an outcome stays unknown.