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

# Metadata & tracing

> Every response carries an X-Trace-Id and an X-Request-Timestamp. Log the trace ID for correlation, retries, and support triage.

Every v3 response (success or error) carries an `X-Trace-Id` and an `X-Request-Timestamp`. `X-Trace-Id` is OpenFX's correlation handle for the request. Log it on every call so you can stitch together your own logs across retries and services, and give OpenFX support the trace they need. For your own per-call correlation, send an optional `X-Request-Id` header. See [Client request correlation](#client-request-correlation). FX withdrawals additionally accept a persistent `metadata` object in the body that outlives the single request. See [Withdrawal metadata](#withdrawal-metadata).

## Response headers

| Header | Type | Purpose |
| - | - | - |
| `X-Trace-Id` | string | The correlation and support/debug handle for this request hop. A valid client `traceparent` or (when no valid `traceparent` is supplied) `X-Trace-Id` is adopted; otherwise the gateway generates a trace ID. Quote it when filing a support ticket. |
| `X-Request-Timestamp` | string (RFC 3339, ms) | The server's UTC timestamp of when the request reached the API service, e.g. `2026-05-29T12:34:56.789Z`. Useful for clock-skew checks and ordering your own logs against server time. |

<Note>
  **Server-side and client-side correlation are separate headers.** `X-Trace-Id`
  is the server-generated request identifier, present on every response; there
  is no server `requestId` field in the response body. `X-Request-Id` is the
  client-side equivalent: optional, and echoed back only when it matches the
  required format. See [Client request correlation](#client-request-correlation). If
  you need a body-portable copy of the server handle (for queued retries or
  webhook envelopes that lose HTTP headers), capture `X-Trace-Id` into your own
  log record at call time.
</Note>

```http theme={"theme":{"light":"poimandres","dark":"poimandres"}}
HTTP/1.1 200 OK
X-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736
X-Request-Timestamp: 2026-05-29T12:34:56.789Z
Content-Type: application/json
```

## Client request correlation

`X-Trace-Id` is the **server's** correlation handle. For **your own** per-call correlation, send an optional `X-Request-Id` header on any request:

| Header | Direction | Purpose |
| - | - | - |
| `X-Request-Id` | Request | Optional, client-chosen. Must contain 1-200 characters from `[A-Za-z0-9_.-]`; a valid value is echoed back. An invalid value returns `400` <span class="ofx-enum">VALIDATION\_HEADER\_FAILED</span>. |

`X-Request-Id` is independent of `X-Trace-Id`: it's client-generated and lives in headers on both the request and the response when valid, for the lifetime of that one HTTP round-trip. There is no server-generated `requestId` field in any response body.

```http theme={"theme":{"light":"poimandres","dark":"poimandres"}}
GET /v3/fx/pairs HTTP/1.1
X-Request-Id: my-req-abc-123

HTTP/1.1 200 OK
X-Request-Id: my-req-abc-123
X-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736
```

<Note>
  **`X-Request-Id` vs `X-Trace-Id`.** `X-Request-Id` is **your** correlation
  value, chosen by your client and echoed back only when you send it.
  `X-Trace-Id` is **OpenFX's** debug/trace handle, present on every response
  (success and error) whether or not you sent an `X-Request-Id`. Include both
  when you log, and quote `X-Trace-Id` when filing a support ticket.
</Note>

## Withdrawal metadata

For bookkeeping that should persist with the resource (not just the one request/response round-trip), `POST /v3/fx/withdrawals` accepts an optional `metadata` object in the request body. This is a separate mechanism from `X-Request-Id`: `X-Request-Id` is per-call and lives in headers; `metadata` is per-resource, persists with the withdrawal, and travels in the body.

Within the FX API (`/v3/fx/...`), `metadata` is accepted only on `POST /v3/fx/withdrawals`. The Payments API accepts its own `metadata` object on [create account](/v3/api-reference/payments/accounts/create-account), [create payout address](/v3/api-reference/payments/addresses/create-payout-address), [create payout quote](/v3/api-reference/payments/payouts/create-payout-quote), and [create payout](/v3/api-reference/payments/payouts/create-payout), with the same key-count, key-length, value-length, and total-size limits as below. Payments metadata keys are also limited to letters, digits, and underscores.

It is a general-purpose, client-owned bag: keys are client-defined strings (for example `metadata.customerId`, `metadata.reconId`). All values must be **strings**.

The server treats `metadata` as **opaque pass-through**: it enforces only size and structure limits, never interprets the contents, persists it with the withdrawal, and returns it nested inside the resource at `data.metadata` on the **success response**, **not** as a top-level sibling of `data`.

**Limits:** at most 50 keys, each key ≤ 40 characters, each value ≤ 500 characters, and the serialized object under 8 KB. A request that exceeds any of these limits is rejected with `400` <span class="ofx-enum">VALIDATION\_BODY\_FAILED</span>.

```json title="Request: POST /v3/fx/withdrawals" theme={"theme":{"light":"poimandres","dark":"poimandres"}}
{
  "withdrawalAccountId": "wac_NDqQ9LmcUASpnHR6CTvdkk",
  "withdrawalAmount": "100.00",
  "currency": "USD",
  "metadata": {
    "customerId": "cus_12345",
    "reconId": "recon-2026-0001"
  }
}
```

The success response nests the `Withdrawal` resource inside `data`, with `metadata` nested inside it, not as a sibling of `data`:

```json title="Success response: metadata nested inside data.metadata, not a sibling of data" theme={"theme":{"light":"poimandres","dark":"poimandres"}}
{
  "data": {
    "object": "withdrawal",
    "id": "wtd_6SATV6VSUdBTttHWBmCYjD",
    "withdrawalAccountId": "wac_NDqQ9LmcUASpnHR6CTvdkk",
    "withdrawalAmount": "100.00",
    "currency": "USD",
    "assetType": "FIAT",
    "status": "PENDING",
    "network": null,
    "actorId": "usr_7m4VsfRw4pGrS76WYj5tnx",
    "actorEmail": "client-user@example.com",
    "createdAt": "2026-04-28T10:00:00.000Z",
    "metadata": {
      "customerId": "cus_12345",
      "reconId": "recon-2026-0001"
    }
  }
}
```

<Warning>
  **Metadata is visible in the OpenFX platform, API, and MCP server, including connected AI tools.** Don't include sensitive or personal data in `metadata`.
</Warning>

<Warning>
  **`metadata` is set at creation and can't be changed afterward.** v3 has no
  `PATCH` endpoints, so a withdrawal's `metadata` is fixed once the withdrawal
  is created. It is also **not currently filterable** on [`GET
      /v3/fx/withdrawals`](/v3/api-reference/trade-settlement/list-withdrawals).
  Fetch and filter client-side.
</Warning>

<Warning>
  **`metadata` is returned on success responses only.** It is **never** echoed
  on error responses. An error envelope carries `code`, `type`, `message`, and
  `details`, with no `metadata`. To correlate a failed call, read the
  `X-Trace-Id` response header (and your own logged copy of the `metadata` you
  sent). On read endpoints (`GET` / list), any `metadata` sent is ignored.
</Warning>

## Encoding

Requests use UTF-8 encoded `application/json`. Responses use `Content-Type: application/json` without a charset parameter. A request body with duplicate JSON keys returns `400` <span class="ofx-enum">VALIDATION\_BODY\_FAILED</span>.

## Timestamp precision

Across v3, timestamp fields such as `X-Request-Timestamp`, `createdAt`, `expiresAt`, and `completedAt` are RFC 3339 / ISO 8601 in UTC with **millisecond** precision and a trailing `Z`. `RateLimit-Reset` and `Retry-After` are not timestamps at all: both are plain integers counting **seconds until you should retry**, per the IETF `RateLimit-*` convention. See [Rate limiting](/v3/rate-limiting).

## Logging pattern

A two-line capture on every request is enough to make support and incident response fast:

<CodeGroup>
  ```javascript Node theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  // Logs X-Trace-Id and X-Request-Timestamp on every call — the pattern from
  // /v3/metadata-and-tracing#logging-pattern, run against a real endpoint.
  // 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");
  // Pulls the two correlation fields off any v3 response. Kept separate from
  // the network call so it's testable without a real request.
  function formatLogEntry(res, op) {
    return {
      traceId: res.headers.get("x-trace-id"),
      serverTime: res.headers.get("x-request-timestamp"),
      op,
      status: res.status,
    };
  }
  async function listPairs() {
    const url = "https://api.openfx.com/v3/fx/pairs";
    const { headers } = signRequest({ method: "GET", url });
    const res = await fetch(url, { headers });
    const data = await res.json();
    console.log(formatLogEntry(res, "list_pairs"));
    return data;
  }
  module.exports = { formatLogEntry, listPairs };
  if (require.main === module) {
    listPairs().catch((err) => {
      console.error("Error:", err.message);
      process.exitCode = 1;
    });
  }
  ```

  ```python Python theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  """Logs X-Trace-Id and X-Request-Timestamp on every call — the pattern from
  /v3/metadata-and-tracing#logging-pattern, run against a real endpoint."""
  import os
  import sys
  # 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
  def format_log_entry(res, op):
      """Pulls the two correlation fields off any v3 response. Kept separate
      from the network call so it's testable without a real request."""
      return {
          "trace_id": res.headers.get("x-trace-id"),
          "server_time": res.headers.get("x-request-timestamp"),
          "op": op,
          "status": res.status_code,
      }
  def list_pairs():
      url = "https://api.openfx.com/v3/fx/pairs"
      headers, _ = sign_request("GET", url)
      res = requests.get(url, headers=headers)
      print(format_log_entry(res, "list_pairs"))
      return res.json()
  if __name__ == "__main__":
      list_pairs()
  ```

  ```java Java theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  // Logs X-Trace-Id and X-Request-Timestamp on every call — the pattern from
  // /v3/metadata-and-tracing#logging-pattern, run against a real endpoint.
  import java.net.HttpURLConnection;
  import java.net.URI;
  import java.util.LinkedHashMap;
  import java.util.Map;
  public class LogTrace {
      // A response reduced to just the fields formatLogEntry needs, so it's
      // testable without a real HttpURLConnection.
      static class ResponseInfo {
          final String traceId;
          final String serverTime;
          final int status;
          ResponseInfo(String traceId, String serverTime, int status) {
              this.traceId = traceId;
              this.serverTime = serverTime;
              this.status = status;
          }
      }
      // Pulls the two correlation fields off any v3 response. Kept separate
      // from the network call so it's testable without a real request.
      static Map<String, Object> formatLogEntry(ResponseInfo res, String op) {
          Map<String, Object> entry = new LinkedHashMap<>();
          entry.put("traceId", res.traceId);
          entry.put("serverTime", res.serverTime);
          entry.put("op", op);
          entry.put("status", res.status);
          return entry;
      }
      public static void main(String[] args) throws Exception {
          String url = "https://api.openfx.com/v3/fx/pairs";
          // 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());
          }
          int status = conn.getResponseCode();
          ResponseInfo info = new ResponseInfo(
                  conn.getHeaderField("X-Trace-Id"), conn.getHeaderField("X-Request-Timestamp"), status);
          System.out.println(formatLogEntry(info, "list_pairs"));
      }
  }
  ```

  ```cpp C++ theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  // Logs X-Trace-Id and X-Request-Timestamp on every call — the pattern from
  // /v3/metadata-and-tracing#logging-pattern, run against a real endpoint.
  // Requires OpenSSL and libcurl.
  #include <curl/curl.h>
  #include <algorithm>
  #include <cctype>
  #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);
  struct ResponseInfo {
      std::string traceId;
      std::string serverTime;
      long status;
  };
  // Pulls the two correlation fields off any v3 response. Kept separate from
  // the network call so it's testable without a real request.
  std::map<std::string, std::string> formatLogEntry(const ResponseInfo& res, const std::string& op) {
      return {
          {"traceId", res.traceId},
          {"serverTime", res.serverTime},
          {"op", op},
          {"status", std::to_string(res.status)},
      };
  }
  static std::string toLower(std::string s) {
      std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); });
      return s;
  }
  static size_t captureHeader(char* buffer, size_t size, size_t nitems, void* userdata) {
      auto* headers = static_cast<std::map<std::string, std::string>*>(userdata);
      std::string line(buffer, size * nitems);
      size_t colon = line.find(':');
      if (colon != std::string::npos) {
          std::string name = toLower(line.substr(0, colon));
          std::string value = line.substr(colon + 1);
          while (!value.empty() && value.front() == ' ') value.erase(0, 1);
          while (!value.empty() && (value.back() == '\r' || value.back() == '\n')) value.pop_back();
          (*headers)[name] = value;
      }
      return size * nitems;
  }
  #ifndef LOG_TRACE_NO_MAIN
  int main() {
      try {
          std::string url = "https://api.openfx.com/v3/fx/pairs";
          SignedRequest req = signRequest("GET", "/v3/fx/pairs", "", "");
          CURL* curl = curl_easy_init();
          std::map<std::string, std::string> respHeaders;
          curl_slist* requestHeaders = nullptr;
          requestHeaders = curl_slist_append(requestHeaders, ("Authorization: " + req.authHeader).c_str());
          requestHeaders = curl_slist_append(requestHeaders, ("X-Request-Signature: " + req.sigHeader).c_str());
          curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
          curl_easy_setopt(curl, CURLOPT_HTTPHEADER, requestHeaders);
          curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, captureHeader);
          curl_easy_setopt(curl, CURLOPT_HEADERDATA, &respHeaders);
          curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](char*, size_t s, size_t n, void*) { return s * n; });
          CURLcode rc = curl_easy_perform(curl);
          if (rc != CURLE_OK) throw std::runtime_error(curl_easy_strerror(rc));
          long status = 0;
          curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
          ResponseInfo info{respHeaders["x-trace-id"], respHeaders["x-request-timestamp"], status};
          for (const auto& [key, value] : formatLogEntry(info, "list_pairs")) {
              std::cout << key << ": " << value << std::endl;
          }
          curl_slist_free_all(requestHeaders);
          curl_easy_cleanup(curl);
          return 0;
      } catch (const std::exception& e) {
          std::cerr << "Error: " << e.what() << std::endl;
          return 1;
      }
  }
  #endif
  ```

  ```go Go theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  // Logs X-Trace-Id and X-Request-Timestamp on every call — the pattern from
  // /v3/metadata-and-tracing#logging-pattern, run against a real endpoint.
  package main
  import (
  	"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"
  )
  // Pulls the two correlation fields off any v3 response. Kept separate from
  // the network call so it's testable without a real request.
  func formatLogEntry(res *http.Response, op string) map[string]interface{} {
  	return map[string]interface{}{
  		"traceId":    res.Header.Get("X-Trace-Id"),
  		"serverTime": res.Header.Get("X-Request-Timestamp"),
  		"op":         op,
  		"status":     res.StatusCode,
  	}
  }
  func listPairs() (map[string]interface{}, error) {
  	url := "https://api.openfx.com/v3/fx/pairs"
  	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()
  	entry := formatLogEntry(res, "list_pairs")
  	fmt.Println(entry)
  	return entry, nil
  }
  func main() {
  	if _, err := listPairs(); err != nil {
  		fmt.Println("Error:", err)
  	}
  }
  ```
</CodeGroup>

<Tip>
  Log `X-Trace-Id` on success and error paths. Success-path logs are often what
  you need to explain a later failure.
</Tip>

## Filing a support ticket

When something goes wrong, include the `X-Trace-Id` from the failing response.

```text theme={"theme":{"light":"poimandres","dark":"poimandres"}}
Subject: Trade execution returning 500 on USDC_USD

Hi OpenFX support,

I'm seeing intermittent 500s on POST /v3/fx/trades.
Latest X-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736
Time: ~2026-04-28 10:00 UTC
```

## Common mistakes

* Reading trace info from the body: the trace handle is the `X-Trace-Id` **header**; there is no body trace field
* Logging only on errors (you lose context for prior successful calls)
* Truncating the trace ID in logs
* Comparing `X-Request-Timestamp` against a local clock without allowing for skew

## What's next

<CardGroup cols={2}>
  <Card title="Errors" icon="https://public-assets.openfx.com/docs/icons/errors.svg" href="/v3/errors">
    When something goes wrong, start with headers and code-based triage.
  </Card>

  <Card title="Rate limiting" icon="https://public-assets.openfx.com/docs/icons/rate-limiting.svg" href="/v3/rate-limiting">
    Headers for back-pressure.
  </Card>
</CardGroup>
