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, withdrawableBalance, 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 JavaScript and in most languages whose numbers are IEEE-754 floats, which is how JSON numbers are typically parsed. For monetary amounts that’s unacceptable: a $0.000000000004 discrepancy compounds across millions of trades. Strings preserve every digit the API returns. FX responses can contain up to 16 fractional digits; parse them with a decimal representation such as Decimal, BigDecimal, or bigint*scale.

Format

An FX request body with a zero amount ("0" or "0.00") or a negative amount (for example, "-1.00") returns 400 VALIDATION_BODY_FAILED. The FX request schemas reject zero and negative amounts before business-rule checks.

Prefix vocabulary

Amount fields are always prefixed to describe what they refer to. There is no bare top-level amount field on any resource 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 withdrawableBalance / totalBalance. The one exception is a nested object that’s already qualified by its parent key: the exposure summary’s usageBreakdown.settlableNow.amount and .settlableLater.amount. Every monetary field carries a qualifier and ends in Amount or Balance. There’s no bare amount field to infer meaning from context.

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.

Withdrawal precision rules per currency type

For withdrawal requests, 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
These per-currency withdrawal checks are business rules; they do not limit FX request or response precision.

Currency codes

Amounts always travel with a currency code (currency, buyCurrency, sellCurrency). 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 too many decimals on a withdrawal. Withdrawal requests allow 2 decimals for fiat and 6 for crypto; over-precision returns 422 WITHDRAWAL_INVALID_AMOUNT_PRECISION with details.maxDecimals and details.currency. Round (don’t truncate) to the right scale before sending. FX amount schemas allow up to 16 fractional digits.
  • Sending a bare amount field. No request body accepts a bare amount field. 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 Trade. quoteAmount / executedAmount is the computed counter-leg amount, not a standalone exchange rate. A bare amount key in a request is rejected (unknown field on the request body).

Balance lifecycle: available vs total

Every Balance row carries two amounts. Treat them as a state machine, not a snapshot.
  • withdrawableBalance: spendable now. Never negative.
  • totalBalance: total held in the currency, including any amount earmarked against PENDING operations (submitted withdrawals, in-flight settlements). Ordinarily withdrawableBalance ≤ totalBalance, but it can go negative when the org owes a settlement in that currency; withdrawableBalance is then 0 rather than a negative-adjacent figure.
The held (earmarked) portion is the difference totalBalance − withdrawableBalance (when totalBalance is non-negative). 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.

Net equity aggregate

GET /v3/fx/balances also returns an aggregate block (nested under data.aggregate) alongside the per-currency data.balances array:
aggregate.withdrawableBalance is the org’s net equity across every currency, converted to USD. It’s the ceiling on what can actually be withdrawn overall, as distinct from any single currency’s withdrawableBalance. It follows the same clamping rule: never negative, 0 when the org’s overall net position is negative.

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 withdrawableBalance 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 cancellation. While PENDING, funds are out of withdrawableBalance but still counted in totalBalance.

Which view to read

What’s next

Quickstart

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

Errors

WITHDRAWAL_INVALID_AMOUNT_PRECISION and other amount-related errors.

FX quotes

Where buyAmount, sellAmount, and quoteAmount come from.