Skip to main content
OpenFX uses ES256-signed JWT bearer tokens plus a body-bound X-Request-Signature header for all v3 API access. The JWT carries identity and is single-use with a 60-second maximum TTL; the request signature binds the call to that specific JWT, request method, path, query, and body. You mint both per request by signing with the name and privateKey from the API key JSON you download from the OpenFX dashboard.
v3 hardens v2 auth — old code does not work unchanged. Two breaks vs v2: (1) the JWT TTL ceiling drops from 120 seconds to 60 seconds — JWTs minted with exp = iat + 120 now return 401 AUTH_TOKEN_INVALID_CONFIG; (2) every v3 request now requires an X-Request-Signature ES256 signature header — calls without it are rejected with 401 AUTH_TOKEN_INVALID. Re-mint with the helpers below; the API key and key file format are unchanged.

JWT shape

A v3 JWT carries the claims and header values below. See Mint a JWT and sign the request for how each language builds these.
JWTs expire after 60 seconds — mint a fresh one per request, not per session. Replaying a nonce is rejected with 401 AUTH_TOKEN_INVALID (details.reason: REPLAYED). Setting exp - iat > 60 seconds returns AUTH_TOKEN_INVALID_CONFIG. A missing X-Request-Signature header is likewise rejected with 401 AUTH_TOKEN_INVALID.

Request signing

Every v3 request carries an X-Request-Signature header — an ES256 signature, IEEE P-1363 fixed 64-byte r‖s encoding, base64url-encoded — over the canonical string:
Sign the canonical string with the same API-key private key that signed the JWT. The server uses the JWT’s kid to look up the matching public key and verifies both the JWT signature and the request signature against it.

Path canonicalization (7 steps)

Apply in order; sign the result. These rules are a strict subset of what our ingress (Istio Envoy with pathNormalization: DEFAULT) applies on the server side, so the path you sign is the path the server hashes.
  1. Empty path becomes /.
  2. Merge consecutive slashes (//a///b/a/b).
  3. Remove dot-segments per RFC 3986 §5.2.4 (/a/./b/../c/a/c).
  4. Do NOT percent-decode — preserve %xx sequences exactly (%2F stays %2F).
  5. Do NOT re-encode any character that wasn’t already encoded.
  6. Preserve case (paths are case-sensitive).
  7. Preserve trailing slash (/a/ and /a are distinct).
Three failure modes you will hit if you skip canonicalization. (1) URL.pathname (or its language equivalent) collapses dot-segments but does NOT merge //, so https://api.openfx.com//v3/fx/pairs signs //v3/fx/pairs while the server hashes /v3/fx/pairs. (2) Re-serializing the JSON body between hashing and sending produces different bytes — the server’s SHA-256 will not match. Serialize once and pass the same bytes to both the hasher and the HTTP client. (3) Default ECDSA encoding in most languages is DER; the server expects IEEE P-1363 fixed 64-byte r‖s. Each sample below uses the language’s idiomatic path to produce IEEE P-1363 output — keep that, don’t substitute the default.

Why nonce reuse and body binding

  • Same nonce in JWT and signature. Reusing the JWT’s nonce value as the signature nonce binds the two together. An attacker who exfiltrates a valid signature cannot pair it with a different JWT because the signature’s canonical string includes the original JWT’s nonce — verifying against a new JWT (with a new nonce) fails.
  • Body binding. Including SHA256_HEX(body) in the canonical string means a captured signature only authorizes the exact request it was signed for. Without this, a stolen bearer token could be paired with an attacker-chosen body until the JWT expired.
  • Server-side nonce uniqueness. The edge reserves each nonce atomically in Valkey for 70 seconds (60s JWT TTL + 10s clock-skew headroom) before the signature is verified. A replayed nonce is rejected with 401 AUTH_TOKEN_INVALID (details.reason: REPLAYED) before any ECDSA work happens — replay attempts cost the attacker a round-trip and gain nothing.

Mint a JWT and sign the request

Each sample mints a fresh 60s JWT and computes the matching X-Request-Signature in a single helper. Treat the helper as signRequest({ method, url, body }) => { headers, body } — call it for every request, not once per session.
Retrieve the key name from the name parameter and the private key from the privateKey parameter in the JSON file downloaded during API key creation. The org ID is encoded inside name (format: org/{org - id}/apiKey/ {api - key - id}).

Common errors

When token minting, signing, or use goes wrong, you’ll see one of these AUTH_* codes. Full catalog in Errors → Auth.

What’s next

Quickstart

Mint a token and make your first trade end-to-end.

Environments

Switch between Sandbox and Live modes with the same key flow.

Errors

AUTH_TOKEN_* codes and how to recover.

Rate limiting

Per-key and per-org limits, plus the headers to watch.