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

# 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). 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. Server-generated on every response. 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 verbatim only when you
  supply it — 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={null}
HTTP/1.1 200 OK
X-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736
X-Request-Timestamp: 2026-05-29T12:34:56.789Z
Content-Type: application/json; charset=utf-8
```

## 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. Echoed back verbatim on the response only when you supply it — omit it and the response omits it too. |

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

```http theme={null}
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.

`metadata` is currently accepted only on `POST /v3/fx/withdrawals` — no other v3 write endpoint accepts it.

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

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

The `Withdrawal` resource returned in `data` is:

```json theme={null}
{
  "object": "withdrawal",
  "id": "wtd_6SATV6VSUdBTttHWBmCYjD",
  "withdrawalAccountId": "wac_NDqQ9LmcUASpnHR6CTvdkk",
  "withdrawalAmount": "100.00",
  "currency": "USD",
  "assetType": "FIAT",
  "status": "PROCESSING",
  "network": null,
  "actorId": "usr_7m4VsfRw4pGrS76WYj5tnx",
  "actorEmail": "client-user@example.com",
  "comments": null,
  "createdAt": "2026-04-28T10:00:00.000Z",
  "metadata": {
    "customerId": "cus_12345",
    "reconId": "recon-2026-0001"
  }
}
```

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

<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

All v3 requests and responses are UTF-8 encoded. Request and response bodies are `application/json; charset=utf-8`; send your request bodies as UTF-8.

## Timestamp precision

Timestamp fields across v3 — `X-Request-Timestamp` and resource fields such as `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={null}
  // 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={null}
  """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={null}
  // 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={null}
  // 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={null}
  // 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={null}
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="triangle-exclamation" href="/v3/errors">
    When something goes wrong, start with headers and code-based triage.
  </Card>

  <Card title="Rate limiting" icon="gauge-high" href="/v3/rate-limiting">
    Headers for back-pressure.
  </Card>
</CardGroup>
