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

# Treasury management

> Hold multi-currency balances, convert between them on demand, withdraw to verified withdrawal accounts only when you actually need to.

Treasury workloads don't always end in a settlement. You hold balances in multiple currencies, rebalance between them as rates move, and only push funds out to a bank or wallet when you need to. v3 supports this with a clean separation: [`GET /v3/fx/balances`](/v3/api-reference/trade-settlement/list-balances) for the read side, the quote-then-trade flow for conversion, and [`POST /v3/fx/withdrawals`](/v3/api-reference/trade-settlement/create-withdrawal) only when you want funds to leave the platform.

## When to reach for this

* A corporate treasurer rebalancing USD / EUR / GBP exposure
* A platform holding customer balances in multiple currencies on a single account
* Same-day intra-portfolio FX without an external settlement leg

If every conversion ends in a payout to a third party, see [Cross-border payments](/v3/examples/cross-border-payments) instead — the trade-then-withdraw flow is the same calls but a different mental model. For a side-by-side comparison of all four patterns, see [Integration patterns](/v3/integration-patterns).

## Setup

<Steps>
  <Step title="Get funded">
    Wire your starting currency into OpenFX. Detection is automatic via [`GET
            /v3/fx/deposits`](/v3/api-reference/trade-settlement/list-deposits) — see
    [Deposit lifecycle](/v3/deposit-lifecycle).
  </Step>

  <Step title="Verify withdrawal accounts (only if you need them)">
    A pure treasury flow doesn't require any verified withdrawal accounts —
    you can hold balances and convert indefinitely without ever calling
    `/v3/fx/withdrawals`. Add accounts only when you know you'll need to pay
    out. See [Verified accounts](/v3/setup/verified-accounts).
  </Step>

  <Step title="(Recommended) Subscribe to deposit webhooks">
    For an automated funding pipeline, subscribe to the `deposits` event so your
    treasury system reconciles incoming wires without polling — the webhook's
    `data` is the Deposit object, so the terminal state lives in `data.status`
    (`COMPLETED`). See [Webhooks setup](/v3/webhooks/setup).
  </Step>
</Steps>

## The flow

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, system-ui, sans-serif','fontSize':'13px','actorBkg':'#ffffff','actorBorder':'#299f68','actorTextColor':'#114330','actorLineColor':'#a7d6bb','signalColor':'#114330','signalTextColor':'#114330','noteBkgColor':'#d7f4e1','noteBorderColor':'#299f68','noteTextColor':'#114330','labelBoxBkgColor':'#114330','labelBoxBorderColor':'#114330','labelTextColor':'#ffffff','activationBkgColor':'#d7f4e1','activationBorderColor':'#299f68'}}}%%
sequenceDiagram
  participant Treasury as 🏛️ Treasury app
  participant OpenFX as 🟢 OpenFX

  Treasury->>+OpenFX: GET /v3/fx/balances
  OpenFX-->>-Treasury: [{ currency: "USD", available: 5_000_000 },<br/>{ currency: "EUR", available: 1_200_000 }, ...]

  Note over Treasury: Decide to convert 500k EUR → USD

  Treasury->>+OpenFX: POST /v3/fx/quotes (EUR → USD)
  OpenFX-->>-Treasury: Quote (rate, expiresAt ≈ +3s)

  Treasury->>+OpenFX: POST /v3/fx/trades (quoteId)
  OpenFX-->>-Treasury: Trade (status: EXECUTED)

  Treasury->>+OpenFX: GET /v3/fx/balances
  OpenFX-->>-Treasury: Updated balances

  Note over Treasury: Optional — only if paying out

  Treasury->>+OpenFX: POST /v3/fx/withdrawals (withdrawalAccountId, withdrawalAmount)
  OpenFX-->>-Treasury: Withdrawal (PENDING)
```

## Step 1 — Read current positions

`GET /v3/fx/balances` returns a `Balance` per currency: `availableBalance` and `totalBalance`. Funds held against pending operations (a withdrawal that hasn't dispatched yet, an in-flight trade) are the difference `totalBalance − availableBalance`, which is not broken out as a separate field in v3; only `availableBalance` is usable for new operations.

```bash cURL theme={null}
curl https://api.openfx.com/v3/fx/balances \
  -H "Authorization: Bearer $OPENFX_JWT" \
  -H "X-Request-Signature: $OPENFX_SIGNATURE"
```

```json theme={null}
{
  "data": [
    {
      "currency": "USD",
      "availableBalance": "5000000.00",
      "totalBalance": "5000000.00"
    },
    {
      "currency": "EUR",
      "availableBalance": "1200000.00",
      "totalBalance": "1200000.00"
    },
    {
      "currency": "GBP",
      "availableBalance": "800000.00",
      "totalBalance": "800000.00"
    }
  ],
  "pagination": {
    "limit": 25,
    "hasNext": false,
    "nextCursor": null,
    "hasPrev": false,
    "prevCursor": null
  }
}
```

## Step 2 — Quote the rebalance

Same quote-then-execute model as every other trade. For treasury workflows you'll usually anchor the **sell side** (the position you're trimming) — `sellAmount` lets you say "convert exactly this much of the over-weight currency":

```bash cURL theme={null}
curl -X POST https://api.openfx.com/v3/fx/quotes \
  -H "Authorization: Bearer $OPENFX_JWT" \
  -H "X-Request-Signature: $OPENFX_SIGNATURE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "buyCurrency": "USD",
    "sellCurrency": "EUR",
    "sellAmount": "500000.00"
  }'
```

The response carries a server-computed `quoteAmount` at the quoted rate — the exact USD you'll receive.

<Tip>
  For larger conversions where 3 seconds isn't enough to get human approval, ask
  for a longer quote window via `quoteForSeconds` (standard durations: 3, 15,
  30, 45, 60).
</Tip>

## Step 3 — Execute

```bash cURL theme={null}
curl -X POST https://api.openfx.com/v3/fx/trades \
  -H "Authorization: Bearer $OPENFX_JWT" \
  -H "X-Request-Signature: $OPENFX_SIGNATURE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "quoteId": "'"$QUOTE_ID"'" }'
```

Atomic: `status: "EXECUTED"` means EUR has decreased and USD has increased by the exact `executedAmount` from the trade (the counter-currency amount received — here, USD, since the quote anchored the EUR `sellAmount`). If `status: "FAILED"`, no balance has moved — see the [`TRADE_EXECUTION_FAILED` recovery rule](/v3/errors#retrying-trade-execution-failed) before retrying.

## Step 4 — Verify the new position

In v3 the trade response does **not** bundle balances. Re-read them separately:

```bash theme={null}
curl https://api.openfx.com/v3/fx/balances \
  -H "Authorization: Bearer $OPENFX_JWT" \
  -H "X-Request-Signature: $OPENFX_SIGNATURE"
```

Assuming the quote returned `quoteAmount: "540000.00"` (the computed USD counter-leg for the 500,000 EUR anchored on `sellAmount`), the −500,000 EUR move corresponds to +540,000 USD in the new balances:

```json theme={null}
{
  "data": [
    {
      "currency": "USD",
      "availableBalance": "5540000.00",
      "totalBalance": "5540000.00"
    },
    {
      "currency": "EUR",
      "availableBalance": "700000.00",
      "totalBalance": "700000.00"
    }
  ],
  "pagination": {
    "limit": 25,
    "hasNext": false,
    "nextCursor": null,
    "hasPrev": false,
    "prevCursor": null
  }
}
```

## Step 5 (optional) — Withdraw

Treasury balances can sit on OpenFX as long as you want them to. Withdraw only when the funds need to be on a counterparty's books — for vendor payments, payroll, or reserves at a bank. The call is the same shape regardless of currency:

```bash cURL theme={null}
curl -X POST https://api.openfx.com/v3/fx/withdrawals \
  -H "Authorization: Bearer $OPENFX_JWT" \
  -H "X-Request-Signature: $OPENFX_SIGNATURE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "withdrawalAccountId": "8ff46b97-5742-4196-88dd-db7b0b46ab7a",
    "withdrawalAmount": "500000.00",
    "currency": "USD",
    "metadata": { "purpose": "Reserve top-up" }
  }'
```

## Putting it together

The read-balances → quote → trade → re-read-balances flow, real `signRequest` and all — plus the optional withdraw from Step 5:

<CodeGroup>
  ```javascript Node theme={null}
  // Read balances -> quote (anchored on the sell side) -> trade -> re-read
  // balances, with an optional withdraw — the pattern from
  // /v3/examples/treasury-management. signRequest is the real helper from
  // /v3/authentication — adjust this path if you've copied it somewhere else.
  const { randomUUID } = require("node:crypto");
  const { signRequest } = require("../../authentication/node/sign-request");
  const BASE_URL = "https://api.openfx.com/v3/fx";
  // Builds the POST /v3/fx/quotes body, anchored on the sell side — trims
  // exactly this much of the over-weight position. Kept separate from the
  // network call so it's testable without a real request.
  function buildQuoteBody(buyCurrency, sellCurrency, sellAmount) {
    return { buyCurrency, sellCurrency, sellAmount };
  }
  // Builds the POST /v3/fx/withdrawals body. Kept separate from the network
  // call so it's testable without a real request.
  function buildWithdrawalBody(withdrawalAccountId, withdrawalAmount, currency, metadata) {
    return { withdrawalAccountId, withdrawalAmount, currency, metadata };
  }
  async function signedFetch(method, url, { idempotencyKey, body } = {}) {
    const bodyStr = body ? JSON.stringify(body) : "";
    const { headers } = signRequest({ method, url, body: bodyStr });
    if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
    const res = await fetch(url, { method, headers, body: body ? bodyStr : undefined });
    const json = await res.json();
    return json.data;
  }
  async function readBalances() {
    return signedFetch("GET", `${BASE_URL}/balances`);
  }
  async function rebalance(buyCurrency, sellCurrency, sellAmount) {
    const balancesBefore = await readBalances();
    const quote = await signedFetch("POST", `${BASE_URL}/quotes`, {
      idempotencyKey: randomUUID(),
      body: buildQuoteBody(buyCurrency, sellCurrency, sellAmount),
    });
    const trade = await signedFetch("POST", `${BASE_URL}/trades`, {
      idempotencyKey: randomUUID(),
      body: { quoteId: quote.id },
    });
    if (trade.status !== "EXECUTED") throw new Error(`Trade ${trade.status}`);
    const balancesAfter = await readBalances();
    return { quoteId: quote.id, tradeId: trade.id, balancesBefore, balancesAfter };
  }
  // Optional — only if paying funds out. See Step 5 on the docs page.
  async function withdraw(withdrawalAccountId, withdrawalAmount, currency, purpose) {
    return signedFetch("POST", `${BASE_URL}/withdrawals`, {
      idempotencyKey: randomUUID(),
      body: buildWithdrawalBody(withdrawalAccountId, withdrawalAmount, currency, { purpose }),
    });
  }
  module.exports = { buildQuoteBody, buildWithdrawalBody, readBalances, rebalance, withdraw };
  if (require.main === module) {
    // Trim EUR 500,000 of the over-weight position into USD.
    rebalance("USD", "EUR", "500000.00").catch((err) => {
      console.error("Error:", err.message);
      process.exitCode = 1;
    });
  }
  ```

  ```python Python theme={null}
  """Read balances -> quote (anchored on the sell side) -> trade -> re-read
  balances, with an optional withdraw — the pattern from
  /v3/examples/treasury-management."""
  import json
  import os
  import sys
  import uuid
  # 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
  BASE_URL = "https://api.openfx.com/v3/fx"
  def build_quote_body(buy_currency, sell_currency, sell_amount):
      """Builds the POST /v3/fx/quotes body, anchored on the sell side —
      trims exactly this much of the over-weight position. Kept separate
      from the network call so it's testable without a real request."""
      return {"buyCurrency": buy_currency, "sellCurrency": sell_currency, "sellAmount": sell_amount}
  def build_withdrawal_body(withdrawal_account_id, withdrawal_amount, currency, metadata):
      """Builds the POST /v3/fx/withdrawals body. Kept separate from the
      network call so it's testable without a real request."""
      return {
          "withdrawalAccountId": withdrawal_account_id,
          "withdrawalAmount": withdrawal_amount,
          "currency": currency,
          "metadata": metadata,
      }
  def signed_request(method, url, idempotency_key=None, body=None):
      body_bytes = json.dumps(body).encode("utf-8") if body is not None else b""
      headers, _ = sign_request(method, url, body_bytes)
      if idempotency_key:
          headers["Idempotency-Key"] = idempotency_key
      res = requests.request(method, url, headers=headers, data=body_bytes if body else None)
      return res.json()["data"]
  def read_balances():
      return signed_request("GET", f"{BASE_URL}/balances")
  def rebalance(buy_currency, sell_currency, sell_amount):
      balances_before = read_balances()
      quote = signed_request(
          "POST",
          f"{BASE_URL}/quotes",
          idempotency_key=str(uuid.uuid4()),
          body=build_quote_body(buy_currency, sell_currency, sell_amount),
      )
      trade = signed_request(
          "POST", f"{BASE_URL}/trades", idempotency_key=str(uuid.uuid4()), body={"quoteId": quote["id"]}
      )
      if trade["status"] != "EXECUTED":
          raise RuntimeError(f"Trade {trade['status']}")
      balances_after = read_balances()
      return {"quoteId": quote["id"], "tradeId": trade["id"], "balancesBefore": balances_before, "balancesAfter": balances_after}
  def withdraw(withdrawal_account_id, withdrawal_amount, currency, purpose):
      """Optional — only if paying funds out. See Step 5 on the docs page."""
      return signed_request(
          "POST",
          f"{BASE_URL}/withdrawals",
          idempotency_key=str(uuid.uuid4()),
          body=build_withdrawal_body(withdrawal_account_id, withdrawal_amount, currency, {"purpose": purpose}),
      )
  if __name__ == "__main__":
      # Trim EUR 500,000 of the over-weight position into USD.
      print(rebalance("USD", "EUR", "500000.00"))
  ```

  ```java Java theme={null}
  // Read balances -> quote (anchored on the sell side) -> trade -> re-read
  // balances, with an optional withdraw — the pattern from
  // /v3/examples/treasury-management. Uses only the built-in JDK APIs — no
  // external dependency, so JSON handling below is intentionally minimal (a
  // flat object serializer and single-field extractors), not a general
  // JSON parser.
  import java.io.InputStream;
  import java.net.HttpURLConnection;
  import java.net.URI;
  import java.nio.charset.StandardCharsets;
  import java.util.LinkedHashMap;
  import java.util.Map;
  import java.util.UUID;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  public class Rebalance {
      static final String BASE_URL = "https://api.openfx.com/v3/fx";
      // Builds the POST /v3/fx/quotes body, anchored on the sell side —
      // trims exactly this much of the over-weight position. Kept separate
      // from the network call so it's testable without a real request.
      static Map<String, String> buildQuoteBody(String buyCurrency, String sellCurrency, String sellAmount) {
          Map<String, String> body = new LinkedHashMap<>();
          body.put("buyCurrency", buyCurrency);
          body.put("sellCurrency", sellCurrency);
          body.put("sellAmount", sellAmount);
          return body;
      }
      // Builds the POST /v3/fx/withdrawals body, with a nested metadata
      // object. Kept separate from the network call so it's testable
      // without a real request.
      static String buildWithdrawalBody(String withdrawalAccountId, String withdrawalAmount, String currency,
                                         String purpose) {
          return "{\"withdrawalAccountId\":\"" + escapeJson(withdrawalAccountId) + "\",\"withdrawalAmount\":\""
                  + escapeJson(withdrawalAmount) + "\",\"currency\":\"" + escapeJson(currency)
                  + "\",\"metadata\":{\"purpose\":\"" + escapeJson(purpose) + "\"}}";
      }
      // Escapes a string for embedding in a JSON string literal. Sufficient
      // for this page's field values; not a general JSON serializer.
      static String escapeJson(String value) {
          StringBuilder out = new StringBuilder();
          for (int i = 0; i < value.length(); i++) {
              char c = value.charAt(i);
              switch (c) {
                  case '"': out.append("\\\""); break;
                  case '\\': out.append("\\\\"); break;
                  case '\n': out.append("\\n"); break;
                  case '\r': out.append("\\r"); break;
                  case '\t': out.append("\\t"); break;
                  default:
                      if (c < 0x20) out.append(String.format("\\u%04x", (int) c));
                      else out.append(c);
              }
          }
          return out.toString();
      }
      static String toJsonObject(Map<String, String> fields) {
          StringBuilder json = new StringBuilder("{");
          boolean first = true;
          for (Map.Entry<String, String> entry : fields.entrySet()) {
              if (!first) json.append(",");
              first = false;
              json.append("\"").append(entry.getKey()).append("\":\"").append(escapeJson(entry.getValue())).append("\"");
          }
          return json.append("}").toString();
      }
      // Extracts one top-level string field from a JSON object by regex.
      // Sufficient for this page's response shapes; not a general JSON parser.
      static String extractField(String json, String field) {
          Matcher m = Pattern.compile("\"" + field + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
          return m.find() ? m.group(1) : null;
      }
      static String signedRequest(String method, String url, String idempotencyKey, String body) throws Exception {
          byte[] bodyBytes = body != null ? body.getBytes(StandardCharsets.UTF_8) : new byte[0];
          // 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(method, url, bodyBytes);
          if (idempotencyKey != null) headers.put("Idempotency-Key", idempotencyKey);
          HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
          conn.setRequestMethod(method);
          for (Map.Entry<String, String> h : headers.entrySet()) {
              conn.setRequestProperty(h.getKey(), h.getValue());
          }
          if (body != null) {
              conn.setDoOutput(true);
              conn.getOutputStream().write(bodyBytes);
          }
          int status = conn.getResponseCode();
          InputStream responseStream = status < 400 ? conn.getInputStream() : conn.getErrorStream();
          // Reads the full response body. extractField below does minimal
          // single-field extraction from it, not a general JSON parser.
          return responseStream == null ? "" : new String(responseStream.readAllBytes(), StandardCharsets.UTF_8);
      }
      static String readBalances() throws Exception {
          return signedRequest("GET", BASE_URL + "/balances", null, null);
      }
      static String[] rebalance(String buyCurrency, String sellCurrency, String sellAmount) throws Exception {
          readBalances();
          String quoteBody = toJsonObject(buildQuoteBody(buyCurrency, sellCurrency, sellAmount));
          String quoteResponse = signedRequest("POST", BASE_URL + "/quotes", UUID.randomUUID().toString(), quoteBody);
          String quoteId = extractField(quoteResponse, "id");
          if (quoteId == null) throw new RuntimeException("Quote response missing id: " + quoteResponse);
          String tradeBody = "{\"quoteId\":\"" + escapeJson(quoteId) + "\"}";
          String tradeResponse = signedRequest("POST", BASE_URL + "/trades", UUID.randomUUID().toString(), tradeBody);
          String tradeStatus = extractField(tradeResponse, "status");
          if (!"EXECUTED".equals(tradeStatus)) throw new RuntimeException("Trade " + tradeStatus);
          readBalances();
          return new String[] {quoteId, extractField(tradeResponse, "id")};
      }
      // Optional — only if paying funds out. See Step 5 on the docs page.
      static void withdraw(String withdrawalAccountId, String withdrawalAmount, String currency, String purpose)
              throws Exception {
          String body = buildWithdrawalBody(withdrawalAccountId, withdrawalAmount, currency, purpose);
          signedRequest("POST", BASE_URL + "/withdrawals", UUID.randomUUID().toString(), body);
      }
      public static void main(String[] args) throws Exception {
          // Trim EUR 500,000 of the over-weight position into USD.
          String[] ids = rebalance("USD", "EUR", "500000.00");
          System.out.println("quoteId=" + ids[0] + " tradeId=" + ids[1]);
      }
  }
  ```

  ```cpp C++ theme={null}
  // Read balances -> quote (anchored on the sell side) -> trade -> re-read
  // balances, with an optional withdraw — the pattern from
  // /v3/examples/treasury-management. Requires OpenSSL and libcurl. No JSON
  // library was added, so JSON handling below is intentionally minimal (a
  // flat object serializer and single-field extractors), not a general
  // JSON parser.
  #include <curl/curl.h>
  #include <openssl/rand.h>
  #include <array>
  #include <iomanip>
  #include <map>
  #include <optional>
  #include <sstream>
  #include <stdexcept>
  #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);
  static const std::string BASE_URL = "https://api.openfx.com/v3/fx";
  // Builds the POST /v3/fx/quotes body, anchored on the sell side — trims
  // exactly this much of the over-weight position. Kept separate from the
  // network call so it's testable without a real request.
  std::map<std::string, std::string> buildQuoteBody(const std::string& buyCurrency, const std::string& sellCurrency,
                                                      const std::string& sellAmount) {
      return {{"buyCurrency", buyCurrency}, {"sellCurrency", sellCurrency}, {"sellAmount", sellAmount}};
  }
  // Builds the POST /v3/fx/withdrawals body, with a nested metadata object.
  // Kept separate from the network call so it's testable without a real
  // request.
  // Escapes a string for embedding in a JSON string literal. Sufficient for
  // this page's field values; not a general JSON serializer.
  std::string escapeJson(const std::string& value) {
      std::ostringstream out;
      for (unsigned char c : value) {
          switch (c) {
              case '"': out << "\\\""; break;
              case '\\': out << "\\\\"; break;
              case '\n': out << "\\n"; break;
              case '\r': out << "\\r"; break;
              case '\t': out << "\\t"; break;
              default:
                  if (c < 0x20) out << "\\u" << std::hex << std::setfill('0') << std::setw(4) << (int)c << std::dec;
                  else out << (char)c;
          }
      }
      return out.str();
  }
  std::string buildWithdrawalBody(const std::string& withdrawalAccountId, const std::string& withdrawalAmount,
                                   const std::string& currency, const std::string& purpose) {
      return "{\"withdrawalAccountId\":\"" + escapeJson(withdrawalAccountId) + "\",\"withdrawalAmount\":\"" +
             escapeJson(withdrawalAmount) + "\",\"currency\":\"" + escapeJson(currency) +
             "\",\"metadata\":{\"purpose\":\"" + escapeJson(purpose) + "\"}}";
  }
  std::string toJsonObject(const std::map<std::string, std::string>& fields) {
      std::ostringstream json;
      json << "{";
      bool first = true;
      for (const auto& [key, value] : fields) {
          if (!first) json << ",";
          first = false;
          json << "\"" << key << "\":\"" << escapeJson(value) << "\"";
      }
      json << "}";
      return json.str();
  }
  // Generates a random per-call idempotency key. Not RFC 4122 UUID format —
  // idempotency keys only need to be unique client-generated strings — but
  // uses the same RAND_bytes source as the JWT nonce in sign_request.cpp.
  std::string randomIdempotencyKey() {
      unsigned char bytes[16];
      RAND_bytes(bytes, sizeof(bytes));
      std::ostringstream hex;
      for (unsigned char b : bytes) hex << std::hex << std::setfill('0') << std::setw(2) << (int)b;
      return hex.str();
  }
  // Extracts one top-level string field from a JSON object with a naive scan.
  // Sufficient for this page's response shapes; not a general JSON parser.
  std::optional<std::string> extractField(const std::string& json, const std::string& field) {
      std::string needle = "\"" + field + "\":\"";
      size_t start = json.find(needle);
      if (start == std::string::npos) return std::nullopt;
      start += needle.size();
      size_t end = json.find('"', start);
      if (end == std::string::npos) return std::nullopt;
      return json.substr(start, end - start);
  }
  static size_t writeToString(char* data, size_t size, size_t nmemb, void* userp) {
      static_cast<std::string*>(userp)->append(data, size * nmemb);
      return size * nmemb;
  }
  std::string signedRequest(const std::string& method, const std::string& path, const std::string& idempotencyKey,
                             const std::string& body) {
      std::string url = BASE_URL + path;
      SignedRequest req = signRequest(method, "/v3/fx" + path, "", body);
      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());
      if (!idempotencyKey.empty()) headers = curl_slist_append(headers, ("Idempotency-Key: " + idempotencyKey).c_str());
      curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
      curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str());
      curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
      if (!body.empty()) curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
      std::string responseBody;
      curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeToString);
      curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody);
      curl_easy_perform(curl);
      curl_slist_free_all(headers);
      curl_easy_cleanup(curl);
      // Returns the raw response body. extractField above does minimal
      // single-field extraction from it, not a general JSON parser.
      return responseBody;
  }
  std::string readBalances() {
      return signedRequest("GET", "/balances", "", "");
  }
  std::array<std::string, 2> rebalance(const std::string& buyCurrency, const std::string& sellCurrency,
                                        const std::string& sellAmount) {
      readBalances();
      std::string quoteBody = toJsonObject(buildQuoteBody(buyCurrency, sellCurrency, sellAmount));
      std::string quoteResponse = signedRequest("POST", "/quotes", randomIdempotencyKey(), quoteBody);
      std::optional<std::string> quoteId = extractField(quoteResponse, "id");
      if (!quoteId) throw std::runtime_error("Quote response missing id: " + quoteResponse);
      std::string tradeBody = "{\"quoteId\":\"" + escapeJson(*quoteId) + "\"}";
      std::string tradeResponse = signedRequest("POST", "/trades", randomIdempotencyKey(), tradeBody);
      std::string tradeStatus = extractField(tradeResponse, "status").value_or("");
      if (tradeStatus != "EXECUTED") throw std::runtime_error("Trade " + tradeStatus);
      readBalances();
      return {*quoteId, extractField(tradeResponse, "id").value_or("")};
  }
  // Optional — only if paying funds out. See Step 5 on the docs page.
  void withdraw(const std::string& withdrawalAccountId, const std::string& withdrawalAmount,
                const std::string& currency, const std::string& purpose) {
      std::string body = buildWithdrawalBody(withdrawalAccountId, withdrawalAmount, currency, purpose);
      signedRequest("POST", "/withdrawals", randomIdempotencyKey(), body);
  }
  #ifndef REBALANCE_NO_MAIN
  #include <iostream>
  int main() {
      try {
          // Trim EUR 500,000 of the over-weight position into USD.
          auto ids = rebalance("USD", "EUR", "500000.00");
          std::cout << "quoteId=" << ids[0] << " tradeId=" << ids[1] << std::endl;
          return 0;
      } catch (const std::exception& e) {
          std::cerr << "Error: " << e.what() << std::endl;
          return 1;
      }
  }
  #endif
  ```

  ```go Go theme={null}
  // Read balances -> quote (anchored on the sell side) -> trade -> re-read
  // balances, with an optional withdraw — the pattern from
  // /v3/examples/treasury-management.
  package main
  import (
  	"bytes"
  	"crypto/rand"
  	"encoding/json"
  	"fmt"
  	"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"
  )
  const baseURL = "https://api.openfx.com/v3/fx"
  type QuoteBody struct {
  	BuyCurrency  string `json:"buyCurrency"`
  	SellCurrency string `json:"sellCurrency"`
  	SellAmount   string `json:"sellAmount"`
  }
  type WithdrawalBody struct {
  	WithdrawalAccountID string            `json:"withdrawalAccountId"`
  	WithdrawalAmount    string            `json:"withdrawalAmount"`
  	Currency            string            `json:"currency"`
  	Metadata            map[string]string `json:"metadata"`
  }
  type apiEnvelope struct {
  	Data json.RawMessage `json:"data"`
  }
  type Quote struct {
  	ID string `json:"id"`
  }
  type Trade struct {
  	ID     string `json:"id"`
  	Status string `json:"status"`
  }
  // Builds the POST /v3/fx/quotes body, anchored on the sell side — trims
  // exactly this much of the over-weight position. Kept separate from the
  // network call so it's testable without a real request.
  func buildQuoteBody(buyCurrency, sellCurrency, sellAmount string) QuoteBody {
  	return QuoteBody{BuyCurrency: buyCurrency, SellCurrency: sellCurrency, SellAmount: sellAmount}
  }
  // Builds the POST /v3/fx/withdrawals body, with a nested Metadata map.
  // Kept separate from the network call so it's testable without a real
  // request.
  func buildWithdrawalBody(withdrawalAccountID, withdrawalAmount, currency, purpose string) WithdrawalBody {
  	return WithdrawalBody{
  		WithdrawalAccountID: withdrawalAccountID,
  		WithdrawalAmount:    withdrawalAmount,
  		Currency:            currency,
  		Metadata:            map[string]string{"purpose": purpose},
  	}
  }
  func newIdempotencyKey() (string, error) {
  	b := make([]byte, 16)
  	if _, err := rand.Read(b); err != nil {
  		return "", err
  	}
  	b[6] = (b[6] & 0x0f) | 0x40
  	b[8] = (b[8] & 0x3f) | 0x80
  	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
  }
  func signedRequest(method, url, idempotencyKey string, body []byte) (json.RawMessage, error) {
  	signed, err := auth.SignRequest(method, url, body)
  	if err != nil {
  		return nil, err
  	}
  	req, err := http.NewRequest(method, url, bytes.NewReader(body))
  	if err != nil {
  		return nil, err
  	}
  	for k, v := range signed.Headers {
  		req.Header.Set(k, v)
  	}
  	if idempotencyKey != "" {
  		req.Header.Set("Idempotency-Key", idempotencyKey)
  	}
  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		return nil, err
  	}
  	defer res.Body.Close()
  	var envelope apiEnvelope
  	if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil {
  		return nil, err
  	}
  	return envelope.Data, nil
  }
  func readBalances() (json.RawMessage, error) {
  	return signedRequest("GET", baseURL+"/balances", "", nil)
  }
  func rebalance(buyCurrency, sellCurrency, sellAmount string) (quoteID, tradeID string, err error) {
  	if _, err := readBalances(); err != nil {
  		return "", "", err
  	}
  	quoteBody, err := json.Marshal(buildQuoteBody(buyCurrency, sellCurrency, sellAmount))
  	if err != nil {
  		return "", "", err
  	}
  	key, err := newIdempotencyKey()
  	if err != nil {
  		return "", "", err
  	}
  	quoteData, err := signedRequest("POST", baseURL+"/quotes", key, quoteBody)
  	if err != nil {
  		return "", "", err
  	}
  	var quote Quote
  	if err := json.Unmarshal(quoteData, &quote); err != nil {
  		return "", "", err
  	}
  	tradeBody, err := json.Marshal(map[string]string{"quoteId": quote.ID})
  	if err != nil {
  		return "", "", err
  	}
  	key, err = newIdempotencyKey()
  	if err != nil {
  		return "", "", err
  	}
  	tradeData, err := signedRequest("POST", baseURL+"/trades", key, tradeBody)
  	if err != nil {
  		return "", "", err
  	}
  	var trade Trade
  	if err := json.Unmarshal(tradeData, &trade); err != nil {
  		return "", "", err
  	}
  	if trade.Status != "EXECUTED" {
  		return "", "", fmt.Errorf("trade %s", trade.Status)
  	}
  	if _, err := readBalances(); err != nil {
  		return "", "", err
  	}
  	return quote.ID, trade.ID, nil
  }
  // Optional — only if paying funds out. See Step 5 on the docs page.
  func withdraw(withdrawalAccountID, withdrawalAmount, currency, purpose string) error {
  	body, err := json.Marshal(buildWithdrawalBody(withdrawalAccountID, withdrawalAmount, currency, purpose))
  	if err != nil {
  		return err
  	}
  	key, err := newIdempotencyKey()
  	if err != nil {
  		return err
  	}
  	_, err = signedRequest("POST", baseURL+"/withdrawals", key, body)
  	return err
  }
  func main() {
  	// Trim EUR 500,000 of the over-weight position into USD.
  	quoteID, tradeID, err := rebalance("USD", "EUR", "500000.00")
  	if err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	fmt.Println("quoteId=", quoteID, "tradeId=", tradeID)
  }
  ```

  ```bash cURL theme={null}
  #!/usr/bin/env bash
  # Read balances -> quote (anchored on the sell side) -> trade -> re-read
  # balances, plus an optional withdraw — the pattern from
  # /v3/examples/treasury-management. Illustrative only: cURL can't compute
  # the ES256 request signature itself, so $OPENFX_JWT/$OPENFX_SIGNATURE below
  # are placeholders — get real values from signRequest, in any supported
  # language, via /v3/authentication.
  set -euo pipefail
  BASE_URL="https://api.openfx.com/v3/fx"
  extract_field() { # $1: JSON, $2: field name
    echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed -E "s/.*:\"([^\"]*)\"/\1/"
  }
  # 1. Read current positions.
  export OPENFX_JWT="<jwt-from-signRequest>"
  export OPENFX_SIGNATURE="<signature-from-signRequest>"
  curl "$BASE_URL/balances" \
    -H "Authorization: Bearer $OPENFX_JWT" \
    -H "X-Request-Signature: $OPENFX_SIGNATURE"
  # 2. Quote the rebalance — trim EUR 500,000 of the over-weight position
  # into USD.
  export OPENFX_JWT="<jwt-from-signRequest>"
  export OPENFX_SIGNATURE="<signature-from-signRequest>"
  QUOTE_RESPONSE=$(curl -s -X POST "$BASE_URL/quotes" \
    -H "Authorization: Bearer $OPENFX_JWT" \
    -H "X-Request-Signature: $OPENFX_SIGNATURE" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "buyCurrency": "USD",
      "sellCurrency": "EUR",
      "sellAmount": "500000.00"
    }')
  QUOTE_ID=$(extract_field "$QUOTE_RESPONSE" "id")
  # 3. Execute.
  export OPENFX_JWT="<jwt-from-signRequest>"
  export OPENFX_SIGNATURE="<signature-from-signRequest>"
  curl -X POST "$BASE_URL/trades" \
    -H "Authorization: Bearer $OPENFX_JWT" \
    -H "X-Request-Signature: $OPENFX_SIGNATURE" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{ "quoteId": "'"$QUOTE_ID"'" }'
  # 4. Verify the new position.
  export OPENFX_JWT="<jwt-from-signRequest>"
  export OPENFX_SIGNATURE="<signature-from-signRequest>"
  curl "$BASE_URL/balances" \
    -H "Authorization: Bearer $OPENFX_JWT" \
    -H "X-Request-Signature: $OPENFX_SIGNATURE"
  # 5. (optional) Withdraw — only if paying funds out.
  export OPENFX_JWT="<jwt-from-signRequest>"
  export OPENFX_SIGNATURE="<signature-from-signRequest>"
  curl -X POST "$BASE_URL/withdrawals" \
    -H "Authorization: Bearer $OPENFX_JWT" \
    -H "X-Request-Signature: $OPENFX_SIGNATURE" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{
      "withdrawalAccountId": "8ff46b97-5742-4196-88dd-db7b0b46ab7a",
      "withdrawalAmount": "500000.00",
      "currency": "USD",
      "metadata": { "purpose": "Reserve top-up" }
    }'
  ```
</CodeGroup>

<div id="reconciliation-tying-deposits--trades--withdrawals" />

## Reconciliation: tying deposits → trades → withdrawals

End-of-day reconciliation answers: "for each dollar that moved, can I trace it?" The minimum reconciliation walks three lists (`/deposits`, `/trades`, `/withdrawals`) plus current `/balances` and proves the equation:

```text theme={null}
opening + deposits_in - withdrawals_out + trade_buys - trade_sells = closing
```

per currency, per day.

### Step 1 — fetch the day's activity

For each list endpoint, page from newest backward to the start of the day. v3 lists are newest-first by `createdAt`; cursor pagination is documented in [Pagination](/v3/pagination). Walk forward (newest → oldest) with `startingAfter`, passing the opaque `pagination.nextCursor` back verbatim until it's `null`:

```javascript JavaScript theme={null}
// `signRequest({ method, url, body })` returns { headers, body } with a freshly minted JWT
// AND a freshly computed X-Request-Signature each call; see Rate limiting → Handling rate limits pattern.
async function fetchAll(path, signRequest) {
  const out = [];
  let cursor = null;
  while (true) {
    const url = new URL(`https://api.openfx.com${path}`);
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("startingAfter", cursor);
    const signed = signRequest({
      method: "GET",
      url: url.toString(),
    });
    const page = await fetch(url, { headers: signed.headers }).then((r) =>
      r.json(),
    );
    out.push(...page.data);
    if (page.pagination.nextCursor === null) break; // end of list
    cursor = page.pagination.nextCursor; // opaque cursor, round-trip verbatim
  }
  return out;
}

const trades = await fetchAll("/v3/fx/trades", signRequest);
const deposits = await fetchAll("/v3/fx/deposits", signRequest);
const withdrawals = await fetchAll("/v3/fx/withdrawals", signRequest);
const balancesUrl = "https://api.openfx.com/v3/fx/balances";
const balancesSig = signRequest({ method: "GET", url: balancesUrl });
const balancesPage = await fetch(balancesUrl, {
  headers: balancesSig.headers,
}).then((r) => r.json());
const balances = balancesPage.data;
```

For incremental "what's new since the last reconciliation run", reverse direction: persist `pagination.prevCursor` after each run and pass it as `endingBefore` next time. The same loop pattern applies — stop when `pagination.prevCursor === null` (you've reached the newest record).

Materialize each into a flat ledger keyed by currency.

### Step 2 — build the per-currency ledger

```ts theme={null}
interface LedgerRow {
  currency: string;
  opening: Decimal;
  deposits_in: Decimal; // sum of deposit.depositAmount where status='COMPLETED'
  withdrawals_out: Decimal; // sum of withdrawal.withdrawalAmount where status='COMPLETED'
  trade_buys: Decimal; // sum of (trade.buyAmount ?? trade.executedAmount) where buyCurrency==currency
  trade_sells: Decimal; // sum of (trade.sellAmount ?? trade.executedAmount) where sellCurrency==currency
  closing: Decimal;
  expected_closing: Decimal; // opening + deposits_in - withdrawals_out + buys - sells
  delta: Decimal; // closing - expected_closing  (should be 0)
}
```

A trade carries exactly one of `buyAmount`/`sellAmount` — whichever side its originating quote was anchored on — with `executedAmount` holding the other side; fall back to `executedAmount` for whichever of the two is absent, or `trade_buys`/`trade_sells` will silently omit every trade anchored on the opposite side. Feed the **string amounts** verbatim into a decimal library (`Decimal.js`, Python `decimal.Decimal`, Java `BigDecimal`). Never coerce to float. See [Amounts](/v3/amounts#parsing-in-clients).

### Step 3 — pair the foreign keys

| Direction                    | Foreign key                                                  | What it gives you                                                                      |
| ---------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| Trade → Quote                | `trade.quoteId` (readable, `qte_`-prefixed)                  | The quote that locked the rate                                                         |
| Withdrawal → Account         | `withdrawal.withdrawalAccountId` (readable, `wac_`-prefixed) | The verified withdrawal account                                                        |
| Withdrawal → Idempotency-Key | The key you persisted                                        | Your row in the [idempotency table](/v3/idempotency#a-persistence-schema-you-can-copy) |

A trade (readable, `tde_`-prefixed) with `quoteId` (readable, `qte_`-prefixed) lets you reconstruct: "at this timestamp, we locked rate X via quote Y and EXECUTED trade Z — A on the anchored side (`buyAmount` or `sellAmount`, whichever the quote locked) and E on the other (`executedAmount`)." That's the full story.

### Step 4 — sanity checks

For each ledger row:

* `delta == 0` per currency. Non-zero means a missing record or off-by-one.
* Every `withdrawal.withdrawalAccountId` is present in `GET /v3/fx/withdrawal-accounts`.
* No long-aged `PENDING` trades. `PENDING` is a transient state during async execution and resolves to `EXECUTED` or `FAILED` within seconds; rows that stay `PENDING` past a small reconciliation window indicate a stuck execution worth investigating.
* `PROCESSING` withdrawals are aged within rail cut-offs ([Settlement times](/v3/settlement-times)).

Quotes are not directly listable in v3 — persist quote responses yourself when you POST so the audit trail is two-sided. At minimum, capture `id`, `quoteAmount`, `buyCurrency`, `sellCurrency`, and `createdAt` at quote time alongside the idempotency key. There is no standalone `rate` field on `Quote` — `quoteAmount` is the counter-leg amount computed at the locked rate. Without persisting it you cannot reconstruct trade history later — `Trade.quoteId` only links back to a quote you can no longer fetch.

```sql theme={null}
-- minimal quote audit table
CREATE TABLE fx_quote_log (
  quoteId         TEXT PRIMARY KEY,           -- readable, typed-prefix ID (qte_ prefix)
  idempotency_key TEXT NOT NULL,
  buyCurrency     TEXT NOT NULL,
  sellCurrency    TEXT NOT NULL,
  quoteAmount     NUMERIC(20, 10) NOT NULL,   -- counter-leg amount, decimal-safe
  createdAt       TIMESTAMPTZ NOT NULL        -- from the quote response
);
```

### Step 5 — handle non-zero deltas

1. **Intra-day timing.** A trade that executes at 23:59:58 and a balance snapshot at 00:00:00 can disagree by exactly one trade. Reconcile against `balances` fetched after the last trade of the day, not at midnight sharp.
2. **In-flight withdrawals.** `PENDING` and `PROCESSING` withdrawals earmark balance but haven't left. Reconcile on `totalBalance` for cash-impact totals; on `availableBalance` for spendable totals. Pick one and stay consistent.
3. **Failed-after-fetch.** A withdrawal can transition `PENDING → FAILED` after you fetched the list. Re-poll `GET /v3/fx/withdrawals/<uuid>` for any non-terminal row older than the rail cut-off.
4. **Pagination boundary.** If `pagination.nextCursor !== null` and you stopped paginating early, you missed records. Page until `nextCursor` is `null`.

### Common mistakes (reconciliation edition)

* **Treating `availableBalance` as the balance.** It excludes funds earmarked against in-flight operations. For end-of-day, `totalBalance` is usually what reconciliation wants.
* **Reconciling on `createdAt` instead of `completedAt`.** A withdrawal created on day N but COMPLETED on day N+1 belongs to day N+1's outflows. Use `completedAt` for cash-impact.
* **Assuming fees on the resource.** v3 does not deduct or surface deposit/withdrawal fees on the resource — there is no fee field, so no fee term enters this reconciliation. Account for any rail/bank fees out of band.

## Common mistakes

* **Confusing `totalBalance` with `availableBalance`.** `availableBalance` ≤ `totalBalance`; the difference is earmarked against in-flight operations. Use `availableBalance` to size new operations; the held portion (`totalBalance − availableBalance`) is locked against in-flight withdrawals.
* **Treating stablecoin balances as chain-specific.** Your `USDC` balance is one number that spans all supported networks — but withdrawals are pinned to one chain via `withdrawalAccountId`. See [Supported networks](/v3/supported-networks).
* **Expecting trade responses to carry balances.** v2 did; v3 doesn't. Read balances separately after a trade.
* **Skipping the deposit webhook.** Polling for incoming wires every minute works for development; in Live, subscribe to the `deposits` event so the treasury system reacts the moment funds clear.

## What's next

<CardGroup cols={2}>
  <Card title="Cross-border payments" icon="globe" href="/v3/examples/cross-border-payments">
    The same calls, ending in a payout to a third party.
  </Card>

  <Card title="Stablecoin on/off ramp" icon="coins" href="/v3/examples/stablecoin-on-off-ramp">
    Convert fiat ↔ stablecoin and deliver on-chain.
  </Card>

  <Card title="Amounts" icon="dollar-sign" href="/v3/amounts">
    String-encoded amounts with semantic prefixes.
  </Card>

  <Card title="Pagination" icon="layer-group" href="/v3/pagination">
    Cursor-based reconciliation walks.
  </Card>
</CardGroup>
