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

# Quickstart

> Get from zero to your first executed trade in under 5 minutes.

This page is the fastest path from an empty terminal to an executed v3 trade. By the end you'll have minted a JWT, called an authenticated endpoint, quoted a 1000 USD → USDC conversion, executed it, and seen the resulting USDC reflected in your balances. Every example chains into the next: the IDs returned in one step are the inputs to the next.

## Before you start

* An OpenFX API key — generate one from the [dashboard](https://app.openfx.com/api-keys) and download the JSON file (contains `name` and `privateKey`). See [Authentication](/v3/authentication) for the file format and how to sign tokens.
* Node.js, Python, or any HTTP client (cURL works fine)
* About 5 minutes

<Tip>
  **Coming from v2?** The high-level flow is unchanged: pairs → quote → trade →
  balances. The wire contract changes materially: URL prefix, response envelope,
  amount types, quote request shape, pagination, and error handling. Use
  [Migration from v2](/v3/migration-from-v2) for the full delta.
</Tip>

## 1. Mint a JWT bearer token

Every request to `/v3/fx/*` carries an `Authorization: Bearer <jwt>` header **and** an `X-Request-Signature` ES256 signature over the request. JWTs are **ES256-signed, single-use, and expire 60 seconds after issuance**. Mint both together, per request, with the `signRequest` helper — see [Mint a JWT and sign the request](/v3/authentication#mint-a-jwt-and-sign-the-request) for the implementation in Node, Python, Java, C++, and Go.

<Warning>
  **The cURL snippets below carry only `Authorization` — they omit
  `X-Request-Signature` because cURL alone can't compute an ES256 signature.**
  They illustrate the wire shape, not a request the API will accept as-is;
  copy-pasting one verbatim returns `401 AUTH_TOKEN_INVALID`. For a request the
  API actually accepts, call `signRequest` from
  [Authentication](/v3/authentication#mint-a-jwt-and-sign-the-request), or run
  [Putting it together](#putting-it-together) below.
</Warning>

```bash theme={null}
# Illustrative only — see the warning above. In real code, call signRequest
# per request instead of exporting one token for the whole session.
export OPENFX_JWT="<jwt-from-signRequest>"
```

<Tip>
  JWTs are single-use (server-enforced nonce uniqueness) and have a 60-second
  TTL. Copy-pasting one twice is rejected with `401 AUTH_TOKEN_INVALID`
  (`details.reason: REPLAYED`). Re-mint between every example below. See
  [Authentication](/v3/authentication) for the helper and signing flow.
</Tip>

<Tip>
  **The `$OPENFX_JWT` env var below is reused across steps for readability — production code mints and signs per request.** The linear walkthrough (pairs → quote → trade → balances) reads more naturally with one exported token, but any sequence that includes user think-time, retries, polling, or pagination must mint a fresh JWT *and* a fresh `X-Request-Signature` per HTTP call. The canonical pattern is in [Rate limiting → Handling rate limits pattern](/v3/rate-limiting#handling-rate-limits-pattern); every retry/poll/pagination sample elsewhere in v3 takes a `signRequest({ method, url, body }) => { headers, body }` parameter rather than a captured token. [Putting it together](#putting-it-together) below shows the real per-call version, in every supported language.
</Tip>

## 2. Verify connectivity

Hit `GET /v3/fx/pairs` (authenticated, no body). If your JWT is good, you'll get back the list of tradable pairs. This is the smoke test that confirms key, signing, and network path all work end-to-end.

```bash cURL theme={null}
curl https://api.openfx.com/v3/fx/pairs \
  -H "Authorization: Bearer $OPENFX_JWT" \
  -H "X-Request-Signature: $OPENFX_SIGNATURE" \
  -D headers.txt
# `headers.txt` now contains X-Trace-Id and the RateLimit-* headers.
```

A successful response contains the complete bounded list of `Pair` resources nested under `data.pairs`. The actual response wraps the object below in `data`; the `PairList` shape itself is:

```json theme={null}
{
  "pairs": [
    {
      "buyCurrency": "USDC",
      "sellCurrency": "USD",
      "minTradeAmount": "10.00",
      "maxTradeAmount": "1000000.00"
    },
    {
      "buyCurrency": "USDT",
      "sellCurrency": "EUR",
      "minTradeAmount": "10.00",
      "maxTradeAmount": "1000000.00"
    }
  ]
}
```

<Tip>
  Every response carries `X-Trace-Id` and `RateLimit-*` in the headers. Capture
  `X-Trace-Id` on every response (success too) so support can reconstruct the
  request later. See [Metadata & tracing](/v3/metadata-and-tracing).
</Tip>

## 3. Quote a trade

`POST /v3/fx/quotes` returns a price guaranteed until `expiresAt` (default 3 seconds). Quotes are a separate step from execution so you can show the user a rate, get confirmation, and execute without surprising them on the price.

State-changing operations need an `Idempotency-Key` header: a client-generated string that lets you safely retry the call. Generate one UUID v4 per logical operation. See [Idempotency](/v3/idempotency).

Pick which side of the trade you want to anchor: supply **`sellAmount`** if you want to spend an exact amount, or **`buyAmount`** if you need to receive an exact amount. Supply exactly one — the server computes the other side at the quoted rate. This example anchors the sell side (1000 USD out).

```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": "USDC",
    "sellCurrency": "USD",
    "sellAmount": "1000.00"
  }'
```

A successful response wraps the `Quote` in `data`, alongside `metadata`. The `Quote` resource itself is:

```json theme={null}
{
  "object": "quote",
  "id": "qte_3FfGK34vwMvVFDedyb2nkf",
  "buyCurrency": "USDC",
  "sellCurrency": "USD",
  "sellAmount": "1000.00",
  "quoteAmount": "999.50",
  "settlementWindow": "T0",
  "expiresAt": "2026-04-28T10:00:03.000Z",
  "createdAt": "2026-04-28T10:00:00.000Z"
}
```

Keep the `id`; you'll pass it to the next step. `sellAmount` is exact (\$1000.00) and `quoteAmount` is the server-computed counter-leg. Notice both amounts are **strings**, not JSON numbers; see [Amounts](/v3/amounts).

## 4. Execute the trade

`POST /v3/fx/trades` executes against the quote ID. You have until `expiresAt` (about 3 seconds from the quote's `createdAt`); after that, the call returns `409 QUOTE_EXPIRED` and you go back to step 3 for a fresh price.

```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": "qte_3FfGK34vwMvVFDedyb2nkf" }'
```

A successful response wraps the `Trade` in `data`, with `status: "EXECUTED"`. The `Trade` resource itself is:

```json theme={null}
{
  "object": "trade",
  "id": "tde_5W7guYdHT24JFnRQrZN9y8",
  "quoteId": "qte_3FfGK34vwMvVFDedyb2nkf",
  "status": "EXECUTED",
  "buyCurrency": "USDC",
  "sellCurrency": "USD",
  "executedAmount": "999.50",
  "clientReferenceId": null,
  "createdAt": "2026-04-28T10:00:00.000Z",
  "settlementWindow": "T0",
  "settlementStartTime": null
}
```

<Warning>
  **One `Idempotency-Key` per logical operation.** The quote and the trade are
  two operations. Generate two keys. Reusing the same key for two different
  trades returns `422 IDEMPOTENCY_MISMATCH`. See [Idempotency](/v3/idempotency).
</Warning>

## 5. Check your balance

`GET /v3/fx/balances` returns a `Balance` per currency. The USDC you just bought should show up in `availableBalance`. In v2 the trade response bundled balances; in v3 you fetch them separately.

```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": "4000.00",
      "totalBalance": "4150.00"
    },
    {
      "currency": "USDC",
      "availableBalance": "999.500000",
      "totalBalance": "999.500000"
    }
  ],
  "pagination": {
    "limit": 25,
    "hasNext": false,
    "nextCursor": null,
    "hasPrev": false,
    "prevCursor": null
  }
}
```

Assuming a starting USD balance of `5150.00`, the trade above debited `1000.00` USD and credited `999.50` USDC, leaving `4150.00` USD total. Your own balances will differ from your trade history.

That's the full round-trip: pairs → quote → trade → balances. The same shape and headers apply to every other v3 endpoint.

## Putting it together

The complete round-trip, real `signRequest` and all — mint a fresh JWT and signature per call, no reused `jwt` symbol:

<CodeGroup>
  ```javascript Node theme={null}
  // The full quickstart round-trip: pairs -> quote -> trade -> balances — the
  // pattern from /v3/quickstart. 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. Supply exactly one of sellAmount or
  // buyAmount to anchor that side. Kept separate from the network call so
  // it's testable without a real request.
  function buildQuoteBody({ buyCurrency, sellCurrency, sellAmount, buyAmount }) {
    const body = { buyCurrency, sellCurrency };
    if (sellAmount !== undefined) body.sellAmount = sellAmount;
    if (buyAmount !== undefined) body.buyAmount = buyAmount;
    return body;
  }
  // Builds the POST /v3/fx/trades body. Kept separate from the network call
  // so it's testable without a real request.
  function buildTradeBody(quoteId) {
    return { quoteId };
  }
  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 { res, data: json.data };
  }
  async function listPairs() {
    const { data } = await signedFetch("GET", `${BASE_URL}/pairs`);
    return data.pairs;
  }
  // A new Idempotency-Key per logical operation — reusing one across a quote
  // and a trade returns 422 IDEMPOTENCY_MISMATCH.
  async function quote(params) {
    const { data } = await signedFetch("POST", `${BASE_URL}/quotes`, {
      idempotencyKey: randomUUID(),
      body: buildQuoteBody(params),
    });
    return data;
  }
  async function executeTrade(quoteId) {
    const { data } = await signedFetch("POST", `${BASE_URL}/trades`, {
      idempotencyKey: randomUUID(),
      body: buildTradeBody(quoteId),
    });
    return data;
  }
  async function getBalances() {
    const { data } = await signedFetch("GET", `${BASE_URL}/balances`);
    return data;
  }
  async function quickstart() {
    const pairs = await listPairs();
    console.log({ step: "pairs", count: pairs.length });
    const quoted = await quote({ buyCurrency: "USDC", sellCurrency: "USD", sellAmount: "1000.00" });
    console.log({ step: "quote", quoteId: quoted.id });
    const trade = await executeTrade(quoted.id);
    console.log({ step: "trade", tradeId: trade.id, status: trade.status });
    const balances = await getBalances();
    console.log({ step: "balances", balances });
  }
  module.exports = { buildQuoteBody, buildTradeBody, listPairs, quote, executeTrade, getBalances, quickstart };
  if (require.main === module) {
    quickstart().catch((err) => {
      console.error("Error:", err.message);
      process.exitCode = 1;
    });
  }
  ```

  ```python Python theme={null}
  """The full quickstart round-trip: pairs -> quote -> trade -> balances — the
  pattern from /v3/quickstart."""
  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=None, buy_amount=None):
      """Builds the POST /v3/fx/quotes body. Supply exactly one of sell_amount
      or buy_amount to anchor that side. Kept separate from the network call
      so it's testable without a real request."""
      body = {"buyCurrency": buy_currency, "sellCurrency": sell_currency}
      if sell_amount is not None:
          body["sellAmount"] = sell_amount
      if buy_amount is not None:
          body["buyAmount"] = buy_amount
      return body
  def build_trade_body(quote_id):
      """Builds the POST /v3/fx/trades body. Kept separate from the network
      call so it's testable without a real request."""
      return {"quoteId": quote_id}
  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 list_pairs():
      return signed_request("GET", f"{BASE_URL}/pairs")["pairs"]
  def quote(buy_currency, sell_currency, sell_amount=None, buy_amount=None):
      # A new Idempotency-Key per logical operation — reusing one across a
      # quote and a trade returns 422 IDEMPOTENCY_MISMATCH.
      return signed_request(
          "POST",
          f"{BASE_URL}/quotes",
          idempotency_key=str(uuid.uuid4()),
          body=build_quote_body(buy_currency, sell_currency, sell_amount, buy_amount),
      )
  def execute_trade(quote_id):
      return signed_request(
          "POST",
          f"{BASE_URL}/trades",
          idempotency_key=str(uuid.uuid4()),
          body=build_trade_body(quote_id),
      )
  def get_balances():
      return signed_request("GET", f"{BASE_URL}/balances")
  def quickstart():
      pairs = list_pairs()
      print({"step": "pairs", "count": len(pairs)})
      quoted = quote("USDC", "USD", sell_amount="1000.00")
      print({"step": "quote", "quoteId": quoted["id"]})
      trade = execute_trade(quoted["id"])
      print({"step": "trade", "tradeId": trade["id"], "status": trade["status"]})
      balances = get_balances()
      print({"step": "balances", "balances": balances})
  if __name__ == "__main__":
      quickstart()
  ```

  ```java Java theme={null}
  // The full quickstart round-trip: pairs -> quote -> trade -> balances — the
  // pattern from /v3/quickstart. 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 Quickstart {
      static final String BASE_URL = "https://api.openfx.com/v3/fx";
      // Builds the POST /v3/fx/quotes body. Supply exactly one of sellAmount
      // or buyAmount to anchor that side. 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, String buyAmount) {
          Map<String, String> body = new LinkedHashMap<>();
          body.put("buyCurrency", buyCurrency);
          body.put("sellCurrency", sellCurrency);
          if (sellAmount != null) body.put("sellAmount", sellAmount);
          if (buyAmount != null) body.put("buyAmount", buyAmount);
          return body;
      }
      // Builds the POST /v3/fx/trades body. Kept separate from the network
      // call so it's testable without a real request.
      static Map<String, String> buildTradeBody(String quoteId) {
          Map<String, String> body = new LinkedHashMap<>();
          body.put("quoteId", quoteId);
          return body;
      }
      // 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();
      }
      // Serializes a flat string-valued map to a JSON object. Sufficient for
      // this page's request bodies; not a general JSON serializer.
      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 listPairs() throws Exception {
          return signedRequest("GET", BASE_URL + "/pairs", null, null);
      }
      // A new Idempotency-Key per logical operation — reusing one across a
      // quote and a trade returns 422 IDEMPOTENCY_MISMATCH.
      static String quote(String buyCurrency, String sellCurrency, String sellAmount, String buyAmount) throws Exception {
          String body = toJsonObject(buildQuoteBody(buyCurrency, sellCurrency, sellAmount, buyAmount));
          return signedRequest("POST", BASE_URL + "/quotes", UUID.randomUUID().toString(), body);
      }
      static String executeTrade(String quoteId) throws Exception {
          String body = toJsonObject(buildTradeBody(quoteId));
          return signedRequest("POST", BASE_URL + "/trades", UUID.randomUUID().toString(), body);
      }
      static String getBalances() throws Exception {
          return signedRequest("GET", BASE_URL + "/balances", null, null);
      }
      public static void main(String[] args) throws Exception {
          listPairs();
          System.out.println("step=pairs");
          String quoteResponse = quote("USDC", "USD", "1000.00", null);
          String quoteId = extractField(quoteResponse, "id");
          if (quoteId == null) throw new RuntimeException("Quote response missing id: " + quoteResponse);
          System.out.println("step=quote quoteId=" + quoteId);
          String tradeResponse = executeTrade(quoteId);
          System.out.println("step=trade tradeId=" + extractField(tradeResponse, "id")
                  + " status=" + extractField(tradeResponse, "status"));
          getBalances();
          System.out.println("step=balances");
      }
  }
  ```

  ```cpp C++ theme={null}
  // The full quickstart round-trip: pairs -> quote -> trade -> balances — the
  // pattern from /v3/quickstart. 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 <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. Supply exactly one of sellAmount or
  // buyAmount to anchor that side. 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,
                                                      std::optional<std::string> sellAmount,
                                                      std::optional<std::string> buyAmount) {
      std::map<std::string, std::string> body{{"buyCurrency", buyCurrency}, {"sellCurrency", sellCurrency}};
      if (sellAmount) body["sellAmount"] = *sellAmount;
      if (buyAmount) body["buyAmount"] = *buyAmount;
      return body;
  }
  // Builds the POST /v3/fx/trades body. Kept separate from the network call
  // so it's testable without a real request.
  std::map<std::string, std::string> buildTradeBody(const std::string& quoteId) {
      return {{"quoteId", quoteId}};
  }
  // 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();
  }
  // Serializes a flat string-valued map to a JSON object. Sufficient for
  // this page's request bodies; not a general JSON serializer.
  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 listPairs() {
      return signedRequest("GET", "/pairs", "", "");
  }
  // A new Idempotency-Key per logical operation — reusing one across a quote
  // and a trade returns 422 IDEMPOTENCY_MISMATCH.
  std::string quote(const std::string& buyCurrency, const std::string& sellCurrency,
                     std::optional<std::string> sellAmount, std::optional<std::string> buyAmount) {
      std::string body = toJsonObject(buildQuoteBody(buyCurrency, sellCurrency, sellAmount, buyAmount));
      return signedRequest("POST", "/quotes", randomIdempotencyKey(), body);
  }
  std::string executeTrade(const std::string& quoteId) {
      std::string body = toJsonObject(buildTradeBody(quoteId));
      return signedRequest("POST", "/trades", randomIdempotencyKey(), body);
  }
  std::string getBalances() {
      return signedRequest("GET", "/balances", "", "");
  }
  #ifndef QUICKSTART_NO_MAIN
  #include <iostream>
  int main() {
      try {
          listPairs();
          std::cout << "step=pairs" << std::endl;
          std::string quoteResponse = quote("USDC", "USD", "1000.00", std::nullopt);
          std::optional<std::string> quoteIdOpt = extractField(quoteResponse, "id");
          if (!quoteIdOpt) throw std::runtime_error("Quote response missing id: " + quoteResponse);
          std::string quoteId = *quoteIdOpt;
          std::cout << "step=quote quoteId=" << quoteId << std::endl;
          std::string tradeResponse = executeTrade(quoteId);
          std::cout << "step=trade tradeId=" << extractField(tradeResponse, "id").value_or("")
                     << " status=" << extractField(tradeResponse, "status").value_or("") << std::endl;
          getBalances();
          std::cout << "step=balances" << std::endl;
          return 0;
      } catch (const std::exception& e) {
          std::cerr << "Error: " << e.what() << std::endl;
          return 1;
      }
  }
  #endif
  ```

  ```go Go theme={null}
  // The full quickstart round-trip: pairs -> quote -> trade -> balances — the
  // pattern from /v3/quickstart.
  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,omitempty"`
  	BuyAmount    string `json:"buyAmount,omitempty"`
  }
  type TradeBody struct {
  	QuoteID string `json:"quoteId"`
  }
  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"`
  }
  type PairList struct {
  	Pairs []json.RawMessage `json:"pairs"`
  }
  // Builds the POST /v3/fx/quotes body. Supply exactly one of sellAmount or
  // buyAmount to anchor that side. Kept separate from the network call so
  // it's testable without a real request.
  func buildQuoteBody(buyCurrency, sellCurrency, sellAmount, buyAmount string) QuoteBody {
  	return QuoteBody{BuyCurrency: buyCurrency, SellCurrency: sellCurrency, SellAmount: sellAmount, BuyAmount: buyAmount}
  }
  // Builds the POST /v3/fx/trades body. Kept separate from the network call
  // so it's testable without a real request.
  func buildTradeBody(quoteID string) TradeBody {
  	return TradeBody{QuoteID: quoteID}
  }
  func newIdempotencyKey() (string, error) {
  	b := make([]byte, 16)
  	if _, err := rand.Read(b); err != nil {
  		return "", err
  	}
  	b[6] = (b[6] & 0x0f) | 0x40 // version 4
  	b[8] = (b[8] & 0x3f) | 0x80 // variant 10
  	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 listPairs() (*PairList, error) {
  	data, err := signedRequest("GET", baseURL+"/pairs", "", nil)
  	if err != nil {
  		return nil, err
  	}
  	var pairs PairList
  	if err := json.Unmarshal(data, &pairs); err != nil {
  		return nil, err
  	}
  	return &pairs, nil
  }
  // A new Idempotency-Key per logical operation — reusing one across a quote
  // and a trade returns 422 IDEMPOTENCY_MISMATCH.
  func quote(buyCurrency, sellCurrency, sellAmount, buyAmount string) (*Quote, error) {
  	body, err := json.Marshal(buildQuoteBody(buyCurrency, sellCurrency, sellAmount, buyAmount))
  	if err != nil {
  		return nil, err
  	}
  	key, err := newIdempotencyKey()
  	if err != nil {
  		return nil, err
  	}
  	data, err := signedRequest("POST", baseURL+"/quotes", key, body)
  	if err != nil {
  		return nil, err
  	}
  	var q Quote
  	if err := json.Unmarshal(data, &q); err != nil {
  		return nil, err
  	}
  	return &q, nil
  }
  func executeTrade(quoteID string) (*Trade, error) {
  	body, err := json.Marshal(buildTradeBody(quoteID))
  	if err != nil {
  		return nil, err
  	}
  	key, err := newIdempotencyKey()
  	if err != nil {
  		return nil, err
  	}
  	data, err := signedRequest("POST", baseURL+"/trades", key, body)
  	if err != nil {
  		return nil, err
  	}
  	var t Trade
  	if err := json.Unmarshal(data, &t); err != nil {
  		return nil, err
  	}
  	return &t, nil
  }
  func getBalances() (json.RawMessage, error) {
  	return signedRequest("GET", baseURL+"/balances", "", nil)
  }
  func main() {
  	pairs, err := listPairs()
  	if err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	fmt.Println("step=pairs count=", len(pairs.Pairs))
  	q, err := quote("USDC", "USD", "1000.00", "")
  	if err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	fmt.Println("step=quote quoteId=", q.ID)
  	t, err := executeTrade(q.ID)
  	if err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	fmt.Println("step=trade tradeId=", t.ID, "status=", t.Status)
  	if _, err := getBalances(); err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	fmt.Println("step=balances")
  }
  ```

  ```bash cURL theme={null}
  #!/usr/bin/env bash
  # The full quickstart round-trip: pairs -> quote -> trade -> balances — the
  # pattern from /v3/quickstart. 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#mint-a-jwt-and-sign-the-request. Mint a
  # fresh pair per call; never reuse one across requests.
  set -euo pipefail
  BASE_URL="https://api.openfx.com/v3/fx"
  # Extracts one top-level string field from a JSON response by pattern match.
  # Sufficient for this script's response shapes; not a general JSON parser.
  extract_field() {
    echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | sed -E "s/.*:\"([^\"]*)\"/\1/"
  }
  # 1. List pairs.
  export OPENFX_JWT="<jwt-from-signRequest>"
  export OPENFX_SIGNATURE="<signature-from-signRequest>"
  curl "$BASE_URL/pairs" \
    -H "Authorization: Bearer $OPENFX_JWT" \
    -H "X-Request-Signature: $OPENFX_SIGNATURE"
  # 2. Quote 1000 USD -> USDC. A new Idempotency-Key per logical operation —
  # reusing one across a quote and a trade returns 422 IDEMPOTENCY_MISMATCH.
  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": "USDC",
      "sellCurrency": "USD",
      "sellAmount": "1000.00"
    }')
  QUOTE_ID=$(extract_field "$QUOTE_RESPONSE" "id")
  # 3. Execute the trade against the quote.
  export OPENFX_JWT="<jwt-from-signRequest>"
  export OPENFX_SIGNATURE="<signature-from-signRequest>"
  TRADE_RESPONSE=$(curl -s -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"'" }')
  echo "trade status: $(extract_field "$TRADE_RESPONSE" "status")"
  # 4. Check the resulting balances.
  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"
  ```
</CodeGroup>

## What's next

<CardGroup cols={2}>
  <Card title="Resource IDs" icon="hashtag" href="/v3/resource-ids">
    Readable, typed-prefix ID format, stability guarantees, and cross-resource
    references.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/v3/errors">
    Typed error codes, retryable flags, and the catch-and-branch pattern for
    your next 5xx.
  </Card>

  <Card title="Trade Settlement" icon="wallet" href="/v3/trade-settlement">
    Deposits, withdrawals, and the unified fiat + stablecoin endpoint.
  </Card>

  <Card title="API reference" icon="code" href="/v3/api-reference/trade/create-quote">
    Every endpoint, every field, every response shape.
  </Card>

  <Card title="Live checklist" icon="list-check" href="/v3/live-checklist">
    Pre-launch checklist, key rotation, IP allowlisting.
  </Card>
</CardGroup>
