Skip to main content
Every monetary amount in v3 (request body, response field, query parameter) is a JSON string with a semantic prefix that tells you what the number refers to: buyAmount, sellAmount, withdrawalAmount, depositAmount, availableBalance, and so on. No floats; no JSON numbers like 1000.50. Sending a number instead of a string returns 400 VALIDATION_BODY_FAILED. Reading the response with a JSON parser that auto-converts to float re-introduces the bug v3 is designed to avoid. This illustrative object combines amount fields from several resource schemas. It is not a standalone request or response and omits the other fields required by those resources.
Strings, always. A request body with "withdrawalAmount": 1000 (JSON number, not string) returns 400 VALIDATION_BODY_FAILED. Sending "1000" works; sending 1000 doesn’t.

Why strings

Floats lose precision. 0.1 + 0.2 is 0.30000000000000004 in JSON (and most languages). For monetary amounts that’s unacceptable: a $0.000000000004 discrepancy compounds across millions of trades. Strings preserve every digit. Each language can parse them into whatever precise representation it uses internally (Decimal, BigDecimal, bigint*scale, etc.). v2 used floats, a regular source of reconciliation bugs.

Format

A request body with "withdrawalAmount": 1000 (number, not string) returns 400 VALIDATION_BODY_FAILED. The same status is returned for a request body with a negative amount (e.g. "withdrawalAmount": "-1.00") — request fields are typed as PositiveAmount and the schema regex rejects the leading sign before any business-rule check runs.

Prefix vocabulary

Amount fields are always prefixed to describe what they refer to. There is no bare amount field anywhere in v3 — every monetary field carries a qualifier and ends in Amount or Balance. A Deposit carries depositAmount, a Withdrawal carries withdrawalAmount, a Quote and Trade carry buyAmount / sellAmount, and a Balance carries availableBalance / totalBalance. This is the inverse of v2, where a Trade had a single bare amount field and you had to infer from context what it referred to. v3 removes the bare-amount field entirely.

Parsing in clients

Sending amounts

Stringify the value before adding it to a JSON body. Most JSON serializers will turn a string into a JSON string with no special handling.

Precision rules per currency type

The server enforces:
  • Fiat: max 2 decimal places. "100.00" ✓, "100.123"422 WITHDRAWAL_INVALID_AMOUNT_PRECISION with details: { maxDecimals: 2 }
  • Crypto: max 6 decimal places. "100.000001" ✓, "100.0000001" → same error with maxDecimals: 6
The envelope pattern allows 8 fractional digits to leave headroom for higher-precision tokens; the per-currency-type rule above is the practical floor.

Currency codes

Amounts always travel with a currency code (currency, buy, sell). Pattern: ^[A-Z0-9]{2,15}$ (some tokens contain digits, e.g. 1INCH).

Common mistakes

  • Sending withdrawalAmount: 1000.50 (number) instead of "1000.50" (string). The number-vs-string distinction is enforced server-side and returns 400 VALIDATION_BODY_FAILED. JSON serializers in some languages quietly emit numbers if you give them a Decimal or BigDecimal; explicitly .toString() before serializing.
  • Parsing into a native float. Round-tripping "1.10" through JSON.parse followed by Number() lands you back in IEEE-754 territory. Wrap in Decimal / BigDecimal immediately on read.
  • Formatting with locale separators. Use the period (.) decimal separator and no thousands separators. A locale-aware formatter that emits "1,000.50" or "1.000,50" fails with 400 VALIDATION_BODY_FAILED. Serialize amounts with an invariant/US formatter, not the user’s locale.
  • Stripping trailing zeros for display before persisting. "1.00" and "1" parse to the same value but are not byte-equivalent strings. If you compare amounts as strings (e.g. for idempotent retries), normalize first.
  • Sending more precision than the currency allows. Fiat tops out at 2 decimal places, crypto at 6. Over-precision returns 422 WITHDRAWAL_INVALID_AMOUNT_PRECISION with details.maxDecimals and details.currency. Round (don’t truncate) to the right scale before sending.
  • Sending a bare amount field. There is no amount field anywhere in v3. A Quote carries buyAmount / sellAmount / quoteAmount; a Trade carries buyAmount / sellAmount / executedAmount; a Withdrawal request and response carry withdrawalAmount; a Deposit carries depositAmount. There is no rate field on either Quote or TradequoteAmount / executedAmount is the computed counter-leg amount, not a standalone exchange rate. A bare amount key is rejected (unknown field on the request body, or simply absent on the response). (v2’s reference_amount is gone — see Migration from v2 → Quote.)

Balance lifecycle: available vs total

Every Balance row carries two amounts. Treat them as a state machine, not a snapshot.
  • availableBalance — spendable now
  • totalBalance — total held in the currency, including any amount earmarked against PENDING operations (submitted withdrawals, in-flight settlements); availableBalancetotalBalance
The held (earmarked) portion is the difference totalBalance − availableBalance. It is not broken out as a separate field in v3; a richer per-state breakdown may be added later.
Quotes do not reserve balance. Only EXECUTED trades and submitted withdrawals do. A quote that never fires is free.

Walkthrough — a trade through its lifecycle

Starting balance: USD 5000.00 total, 5000.00 available. Trades are single-hop atomic — they debit one currency and credit another in the same operation. There is no intermediate held state for trades themselves, so availableBalance and totalBalance move together.

Walkthrough — a withdrawal through its lifecycle

Starting balance: USD 4000.00 total, 4000.00 available. Withdrawals earmark funds at POST and release them at completion or failure. While PENDING or PROCESSING, funds are out of availableBalance but still counted in totalBalance.

Which view to read

Common mistakes (balance edition)

  • Trading against totalBalance. You might “have” USD 5000 on the books but only USD 2000 spendable — trading against total will hit TRADE_INSUFFICIENT_BALANCE mid-day. (Trades and withdrawals use distinct, domain-scoped codes for insufficient balance; see the Errors page for both.)
  • Assuming the held difference equals PENDING withdrawals. The gap between totalBalance and availableBalance includes any in-flight operation that locks balance — PENDING settlements, holds, multi-step transfers all count.
  • Polling GET /v3/fx/balances after every trade. Trades return their post-trade state on the response. Use the canonical balance endpoint for snapshots, not for per-operation confirmation.

What’s next

Quickstart

A worked quote-then-trade flow that uses these amount fields.

Migration from v2

The float-to-string switch and how to convert client code.

Errors

WITHDRAWAL_INVALID_AMOUNT_PRECISION and other amount-related errors.

Trade

Where buyAmount, sellAmount, and quoteAmount come from.