Skip to content

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.

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.

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 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.

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.