API conventions
The rules that hold across every endpoint, stated once. Everything here is enforced by the server; none of it is style advice.
Amounts are human-readable decimal strings
Section titled “Amounts are human-readable decimal strings”Every monetary field is a decimal string in whole asset units: "50" means 50 USDT, "0.5"
means half an ETH. Never raw blockchain units (no sun, no wei).
- On input, trailing zeros are optional:
"100","100.0"and"100.000000"are the same value. - On output, the API pads to the asset’s precision: you read back
"100.000000". - Do the math as decimals, never as floats. Every asset has a fixed precision, and one is a trap: USDT on BSC has 18 decimals (it is Binance-Peg BSC-USD), unlike USDT on TRON and Ethereum (6). If you hard-code 6, BSC amounts will be wrong by 12 orders of magnitude. The per-asset table is on the API reference overview.
Idempotency: retries that can never double-spend
Section titled “Idempotency: retries that can never double-spend”The creating POSTs (create a wallet, create a customer, request a withdrawal) accept an
Idempotency-Key header (1-255 chars, A-Z a-z 0-9 _ -; a UUIDv4 is the easy choice). The first request executes; a retry with the same key and body returns the original
response, flagged Idempotent-Replay: true, without executing again.
| You send | You get |
|---|---|
| Same key, same body | The original response, Idempotent-Replay: true |
| Same key, different body | 422 IDEMPOTENCY_MISMATCH |
| Same key while the first is in flight | 409 IDEMPOTENCY_CONFLICT: wait, retry the same key |
| A malformed key | 400 IDEMPOTENCY_KEY_INVALID |
Keys are remembered for 24 hours. Only definitive outcomes are replayed (success and
deterministic client errors); a 401, 429 or 5xx releases the key so the retry can execute.
Generate the key once per intent and persist it before sending; the worked example is in
Send withdrawals.
Pagination
Section titled “Pagination”List endpoints take page (from 1, default 1) and limit (1-100, default 20) and answer one
envelope:
{ "data": [], "total": 137, "page": 1, "limit": 20 }Iterate until page * limit >= total. Lists are ordered newest first.
Rate limits
Section titled “Rate limits”Requests are rate-limited per API key; the deployed default is 100 requests per rolling 60
seconds (treat the number as today’s value, not a contract). Beyond it you get
429 RATE_LIMITED: back off and retry, and spread bursts rather than hammering the window edge.
Request size
Section titled “Request size”Request bodies are capped at 50 kb; beyond that, 413 PAYLOAD_TOO_LARGE. No legitimate call
approaches the cap; hitting it usually means something (like a metadata blob) does not belong in
the request.
Errors carry a code; warnings are not errors
Section titled “Errors carry a code; warnings are not errors”Every error body is the same envelope, and the code is the contract:
{ "error": "Insufficient balance. Available: 10.000000, Requested: 50.000000", "code": "INSUFFICIENT_BALANCE" }Branch on code, log error, and expect the generic codes (VALIDATION_ERROR, NOT_FOUND,
UNPROCESSABLE…) far more often than named ones; the full catalogue with retry guidance is in
Error codes.
Separately, a successful response can carry a warnings[] array (the fee quote does this: a quote
can be well-formed yet not viable). A warning never changes the status code; read it, do not treat
it as a failure. Warning codes are additive over time, ignore the ones you do not know.
Additive evolution
Section titled “Additive evolution”Successful responses gain fields over time; unknown fields and unknown enum values must be ignored, not rejected. What is and is not promised long-term is stated narrowly in Backwards compatibility.