> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs-v3.openfx.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Amounts

> Format rules, prefix vocabulary, parsing examples, and per-currency precision limits for monetary values in v3.

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.

```json theme={null}
{
  "buyAmount": "1000.00",
  "sellAmount": "999.50",
  "availableBalance": "5000.00",
  "totalBalance": "5150.00"
}
```

<Warning>
  **Strings, always.** A request body with `"withdrawalAmount": 1000` (JSON
  number, not string) returns `400 VALIDATION_BODY_FAILED`. Sending `"1000"`
  works; sending `1000` doesn't.
</Warning>

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

| Constraint                                         |                                                                                                                                                                                                                                                                                                                                    |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pattern (requests + non-reversible fields)         | `^[0-9]+(\.[0-9]{1,8})?$` — schema `PositiveAmount`. Used for every request body amount field and for response/limit fields that are never negative (balances, withdrawal amounts, quote/trade amounts and rates, trade-limit error details).                                                                                      |
| Pattern (response fields that may carry reversals) | `^-?[0-9]+(\.[0-9]{1,8})?$` — schema `SignedAmount`. Used **only** on response fields where a negative value is semantically meaningful. Today the sole consumer is `Deposit.depositAmount`.                                                                                                                                       |
| Max fractional digits                              | 8 (envelope)                                                                                                                                                                                                                                                                                                                       |
| Practical fiat precision                           | 2 decimals (`"1000.00"`)                                                                                                                                                                                                                                                                                                           |
| Practical crypto precision                         | 6 decimals (`"1000.123456"`)                                                                                                                                                                                                                                                                                                       |
| Leading zero required                              | Yes (`"0.50"`, not `".50"`)                                                                                                                                                                                                                                                                                                        |
| Scientific notation                                | Not allowed (`1e3` is invalid)                                                                                                                                                                                                                                                                                                     |
| Decimal separator                                  | Period (`.`) only — the US / invariant format.                                                                                                                                                                                                                                                                                     |
| Thousands separators                               | Not allowed. No commas, spaces, underscores, or apostrophes between digits. `"1,000.50"`, `"1.000,50"` (European), and `"1 000.50"` all fail with `400 VALIDATION_BODY_FAILED`.                                                                                                                                                    |
| Trailing zeros                                     | Allowed, preserved (`"1.00"` ≠ `"1"` for display purposes, but both parse to the same value)                                                                                                                                                                                                                                       |
| Negative amounts                                   | Allowed in response fields only — and only on fields typed as `SignedAmount` (canonical example: `Deposit.depositAmount`, which carries late rail reversals as new negative-amount Deposit events). Never accepted in request bodies; a request body with a leading `-` fails schema validation with `400 VALIDATION_BODY_FAILED`. |

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

| Prefix             | Meaning                                                                                                                                                                                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `buyAmount`        | The amount being bought / received. On `POST /v3/fx/quotes`, supply this when you want to lock the **buy** side (you choose what to receive); the server computes `sellAmount`.                                                                  |
| `sellAmount`       | The amount being sold / debited. On `POST /v3/fx/quotes`, supply this when you want to lock the **sell** side (you choose what to spend); the server computes `buyAmount`. Supply exactly one of `buyAmount` or `sellAmount` on a quote request. |
| `withdrawalAmount` | The amount to withdraw, on a `POST /v3/fx/withdrawals` request body and on the `Withdrawal` response. Always positive (`PositiveAmount`).                                                                                                        |
| `depositAmount`    | The amount that landed, on a `Deposit` response. May be negative (`SignedAmount`) when a late rail reversal surfaces as a new negative-amount Deposit event.                                                                                     |
| `availableBalance` | Funds available for trading or withdrawing right now (on a `Balance` row)                                                                                                                                                                        |
| `totalBalance`     | Total funds held in a currency, including any amount earmarked against pending operations (on a `Balance` row); `availableBalance` ≤ `totalBalance`                                                                                              |
| `minTradeAmount`   | Smallest tradable amount on a pair, denominated in `sellCurrency`                                                                                                                                                                                |
| `maxTradeAmount`   | Largest tradable amount on a pair, denominated in `sellCurrency`                                                                                                                                                                                 |

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

<CodeGroup>
  ```javascript Node theme={null}
  // Parses buyAmount/sellAmount as Decimal, not float, to avoid the precision
  // loss floats introduce — the pattern from /v3/amounts#parsing-in-clients.
  const Decimal = require("decimal.js");
  // signRequest is the real helper from /v3/authentication — adjust this path
  // if you've copied it somewhere else in your own project.
  const { signRequest } = require("../../authentication/node/sign-request");
  // Divides two string amount fields as Decimal. Kept separate from the
  // network call so it's testable without a real request. Exactly one of
  // buyAmount/sellAmount is present on a trade — whichever side the quote
  // was anchored on — and executedAmount carries the other side.
  function computeRate(trade) {
    const buy = new Decimal(trade.buyAmount ?? trade.executedAmount);
    const sell = new Decimal(trade.sellAmount ?? trade.executedAmount);
    return buy.div(sell);
  }
  async function getTrade(id) {
    const url = `https://api.openfx.com/v3/fx/trades/${id}`;
    const { headers } = signRequest({ method: "GET", url });
    const res = await fetch(url, { headers });
    const { data } = await res.json();
    return data;
  }
  async function tradeRate(id) {
    const trade = await getTrade(id);
    return computeRate(trade);
  }
  module.exports = { computeRate, getTrade, tradeRate };
  if (require.main === module) {
    tradeRate("some-trade-id").catch((err) => {
      console.error("Error:", err.message);
      process.exitCode = 1;
    });
  }
  ```

  ```python Python theme={null}
  """Parses buyAmount/sellAmount as Decimal, not float, to avoid the precision
  loss floats introduce — the pattern from /v3/amounts#parsing-in-clients."""
  import os
  import sys
  from decimal import Decimal, getcontext
  # sign_request is the real helper from /v3/authentication — adjust this path
  # if you've copied it somewhere else in your own project.
  sys.path.insert(
      0, os.path.join(os.path.dirname(__file__), "..", "..", "authentication", "python")
  )
  from sign_request import sign_request  # noqa: E402
  import requests  # noqa: E402
  getcontext().prec = 28
  def compute_rate(trade):
      """Divides two string amount fields as Decimal. Kept separate from the
      network call so it's testable without a real request. Exactly one of
      buyAmount/sellAmount is present on a trade — whichever side the quote
      was anchored on — and executedAmount carries the other side."""
      buy = Decimal(trade.get("buyAmount", trade["executedAmount"]))
      sell = Decimal(trade.get("sellAmount", trade["executedAmount"]))
      return buy / sell
  def get_trade(trade_id):
      url = f"https://api.openfx.com/v3/fx/trades/{trade_id}"
      headers, _ = sign_request("GET", url)
      res = requests.get(url, headers=headers)
      return res.json()["data"]
  def trade_rate(trade_id):
      trade = get_trade(trade_id)
      return compute_rate(trade)
  if __name__ == "__main__":
      print(trade_rate("some-trade-id"))
  ```

  ```java Java theme={null}
  // Parses buyAmount/sellAmount as BigDecimal, not double, to avoid the
  // precision loss floats introduce — the pattern from
  // /v3/amounts#parsing-in-clients.
  import java.math.BigDecimal;
  import java.math.MathContext;
  import java.net.HttpURLConnection;
  import java.net.URI;
  import java.util.Map;
  public class ParseAmounts {
      // Divides two string amount fields as BigDecimal. Kept separate from
      // the network call so it's testable without a real request. Exactly
      // one of buyAmount/sellAmount is present on a trade — whichever side
      // the quote was anchored on — and executedAmount carries the other
      // side.
      static BigDecimal computeRate(Map<String, String> trade) {
          String buyAmount = trade.getOrDefault("buyAmount", trade.get("executedAmount"));
          String sellAmount = trade.getOrDefault("sellAmount", trade.get("executedAmount"));
          BigDecimal buy = new BigDecimal(buyAmount);
          BigDecimal sell = new BigDecimal(sellAmount);
          return buy.divide(sell, new MathContext(28));
      }
      static Map<String, String> getTrade(String id) throws Exception {
          String url = "https://api.openfx.com/v3/fx/trades/" + id;
          // SignRequest is the real helper from /v3/authentication, compiled
          // on the classpath alongside this file — see that page's example.
          Map<String, String> headers = SignRequest.signRequest("GET", url, new byte[0]);
          HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
          conn.setRequestMethod("GET");
          for (Map.Entry<String, String> h : headers.entrySet()) {
              conn.setRequestProperty(h.getKey(), h.getValue());
          }
          conn.getResponseCode();
          // A real implementation would parse the JSON response body's `data`
          // object into a Map here.
          return Map.of();
      }
      public static void main(String[] args) throws Exception {
          Map<String, String> trade = getTrade("some-trade-id");
          System.out.println(computeRate(trade));
      }
  }
  ```

  ```cpp C++ theme={null}
  // Parses buyAmount/sellAmount as a fixed-point decimal, not double, to avoid
  // the precision loss floats introduce — the pattern from
  // /v3/amounts#parsing-in-clients. Requires OpenSSL and libcurl.
  #include <curl/curl.h>
  #include <cstdint>
  #include <iostream>
  #include <map>
  #include <string>
  // Matches the definition in ../../authentication/cpp/sign_request.cpp,
  // compiled together with this file — see that file for the real
  // implementation of signRequest.
  struct SignedRequest {
      std::string authHeader;
      std::string sigHeader;
  };
  SignedRequest signRequest(const std::string& method, const std::string& urlPath,
                             const std::string& query, const std::string& body);
  // A fixed-point decimal: value * 10^-scale. Avoids the precision loss a
  // double introduces when dividing decimal string amounts.
  struct Decimal {
      int64_t value;
      int scale;
  };
  static Decimal parseDecimalString(const std::string& s) {
      size_t dot = s.find('.');
      if (dot == std::string::npos) return {std::stoll(s), 0};
      std::string digits = s.substr(0, dot) + s.substr(dot + 1);
      return {std::stoll(digits), static_cast<int>(s.size() - dot - 1)};
  }
  // std::to_string does not support __int128 — convert by hand.
  static std::string int128ToString(__int128 value) {
      if (value == 0) return "0";
      std::string result;
      while (value > 0) {
          result.insert(result.begin(), '0' + static_cast<int>(value % 10));
          value /= 10;
      }
      return result;
  }
  // Returns trade[key] if present, else trade["executedAmount"]. Exactly one
  // of buyAmount/sellAmount is present on a trade — whichever side the quote
  // was anchored on — and executedAmount carries the other side.
  static const std::string& amountOrExecuted(const std::map<std::string, std::string>& trade,
                                              const std::string& key) {
      auto it = trade.find(key);
      return it != trade.end() ? it->second : trade.at("executedAmount");
  }
  // Divides two string amount fields as fixed-point decimals, scaled to 20
  // fractional digits. Kept separate from the network call so it's testable
  // without a real request.
  std::string computeRate(const std::map<std::string, std::string>& trade) {
      Decimal buy = parseDecimalString(amountOrExecuted(trade, "buyAmount"));
      Decimal sell = parseDecimalString(amountOrExecuted(trade, "sellAmount"));
      const int outScale = 20;
      __int128 numerator = static_cast<__int128>(buy.value);
      for (int i = 0; i < outScale + sell.scale - buy.scale; i++) numerator *= 10;
      __int128 quotient = numerator / sell.value;
      std::string digits = int128ToString(quotient);
      while (static_cast<int>(digits.size()) <= outScale) digits = "0" + digits;
      return digits.substr(0, digits.size() - outScale) + "." + digits.substr(digits.size() - outScale);
  }
  static size_t discard(char*, size_t s, size_t n, void*) { return s * n; }
  std::map<std::string, std::string> getTrade(const std::string& id) {
      std::string url = "https://api.openfx.com/v3/fx/trades/" + id;
      SignedRequest req = signRequest("GET", "/v3/fx/trades/" + id, "", "");
      CURL* curl = curl_easy_init();
      curl_slist* headers = nullptr;
      headers = curl_slist_append(headers, ("Authorization: " + req.authHeader).c_str());
      headers = curl_slist_append(headers, ("X-Request-Signature: " + req.sigHeader).c_str());
      curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
      curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
      curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discard);
      curl_easy_perform(curl);
      curl_slist_free_all(headers);
      curl_easy_cleanup(curl);
      // A real implementation would parse the JSON response body's `data`
      // object here.
      return {};
  }
  #ifndef PARSE_AMOUNTS_NO_MAIN
  int main() {
      try {
          auto trade = getTrade("some-trade-id");
          std::cout << computeRate(trade) << std::endl;
          return 0;
      } catch (const std::exception& e) {
          std::cerr << "Error: " << e.what() << std::endl;
          return 1;
      }
  }
  #endif
  ```

  ```go Go theme={null}
  // Parses buyAmount/sellAmount as big.Rat, not float64, to avoid the
  // precision loss floats introduce — the pattern from
  // /v3/amounts#parsing-in-clients.
  package main
  import (
  	"fmt"
  	"math/big"
  	"net/http"
  	// auth is the real signRequest helper from /v3/authentication — see
  	// go.mod's replace directive if you've copied it somewhere else.
  	auth "openfx-v3-authentication-example"
  )
  // amountOrExecuted returns trade[key] if present, else trade["executedAmount"].
  // Exactly one of buyAmount/sellAmount is present on a trade — whichever side
  // the quote was anchored on — and executedAmount carries the other side.
  func amountOrExecuted(trade map[string]string, key string) string {
  	if value, ok := trade[key]; ok {
  		return value
  	}
  	return trade["executedAmount"]
  }
  // Divides two string amount fields as big.Rat. Kept separate from the
  // network call so it's testable without a real request.
  func computeRate(trade map[string]string) (*big.Rat, error) {
  	buyAmount := amountOrExecuted(trade, "buyAmount")
  	sellAmount := amountOrExecuted(trade, "sellAmount")
  	buy, ok := new(big.Rat).SetString(buyAmount)
  	if !ok {
  		return nil, fmt.Errorf("invalid buyAmount: %q", buyAmount)
  	}
  	sell, ok := new(big.Rat).SetString(sellAmount)
  	if !ok {
  		return nil, fmt.Errorf("invalid sellAmount: %q", sellAmount)
  	}
  	return new(big.Rat).Quo(buy, sell), nil
  }
  func getTrade(id string) (map[string]string, error) {
  	url := "https://api.openfx.com/v3/fx/trades/" + id
  	signed, err := auth.SignRequest("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	req, err := http.NewRequest("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	for k, v := range signed.Headers {
  		req.Header.Set(k, v)
  	}
  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		return nil, err
  	}
  	defer res.Body.Close()
  	// A real implementation would parse the JSON response body's `data`
  	// object here.
  	return map[string]string{}, nil
  }
  func main() {
  	trade, err := getTrade("some-trade-id")
  	if err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	rate, err := computeRate(trade)
  	if err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	fmt.Println(rate.FloatString(20))
  }
  ```
</CodeGroup>

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

```javascript theme={null}
await fetch("https://api.openfx.com/v3/fx/withdrawals", {
  method: "POST",
  headers: {/* ... */},
  body: JSON.stringify({
    withdrawalAccountId: "...",
    withdrawalAmount: "1000.00", // string, not 1000.00
    currency: "USD",
  }),
});
```

## 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`).

| Type   | Format                         | Examples               |
| ------ | ------------------------------ | ---------------------- |
| Fiat   | ISO 4217 (3 uppercase letters) | `USD`, `EUR`, `GBP`    |
| Crypto | Canonical ticker               | `USDC`, `USDT`, `EURC` |

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 `Trade` — `quoteAmount` / `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](/v3/migration-from-v2#field-renames-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); `availableBalance` ≤ `totalBalance`

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.

<Note>
  **Quotes do not reserve balance.** Only EXECUTED trades and submitted
  withdrawals do. A quote that never fires is free.
</Note>

### Walkthrough — a trade through its lifecycle

Starting balance: USD `5000.00` total, `5000.00` available.

| Action                                    | `availableBalance` | `totalBalance` | Note                                                      |
| ----------------------------------------- | ------------------ | -------------- | --------------------------------------------------------- |
| Initial                                   | `5000.00`          | `5000.00`      | Idle                                                      |
| `POST /v3/fx/quotes` (USD 1000 → EUR)     | `5000.00`          | `5000.00`      | Quotes price; they don't hold balance                     |
| `POST /v3/fx/trades` (executes the quote) | `4000.00`          | `4000.00`      | Atomic single-hop — USD debited and EUR credited together |
| Quote expires unused                      | `5000.00`          | `5000.00`      | If you never EXECUTED                                     |

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.

| Action                               | `availableBalance` | `totalBalance` | Held (`total − available`) | Withdrawal status               |
| ------------------------------------ | ------------------ | -------------- | -------------------------- | ------------------------------- |
| Initial                              | `4000.00`          | `4000.00`      | `0.00`                     | —                               |
| `POST /v3/fx/withdrawals` (USD 1500) | `2500.00`          | `4000.00`      | `1500.00`                  | `PENDING`                       |
| Withdrawal submitted to rail         | `2500.00`          | `4000.00`      | `1500.00`                  | `PROCESSING`                    |
| Rail confirms settlement             | `2500.00`          | `2500.00`      | `0.00`                     | `COMPLETED` — funds left OpenFX |
| (Counterfactual) rail rejects        | `4000.00`          | `4000.00`      | `0.00`                     | `FAILED` — hold released        |

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

| Use case                           | Look at                                                                                                                              |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| "Can I trade USD 3000 right now?"  | `availableBalance`                                                                                                                   |
| "How much USD do I own in total?"  | `totalBalance`                                                                                                                       |
| "What's tied up in in-flight ops?" | `totalBalance − availableBalance`                                                                                                    |
| End-of-day reconciliation          | `totalBalance` (or both — see [Reconciliation](/v3/examples/treasury-management#reconciliation-tying-deposits--trades--withdrawals)) |

### 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](/v3/errors) 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

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/v3/quickstart#3-quote-a-trade">
    A worked quote-then-trade flow that uses these amount fields.
  </Card>

  <Card title="Migration from v2" icon="arrow-right-arrow-left" href="/v3/migration-from-v2#amounts-are-strings">
    The float-to-string switch and how to convert client code.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/v3/errors">
    `WITHDRAWAL_INVALID_AMOUNT_PRECISION` and other amount-related errors.
  </Card>

  <Card title="Trade" icon="arrows-rotate" href="/v3/trading">
    Where `buyAmount`, `sellAmount`, and `quoteAmount` come from.
  </Card>
</CardGroup>
