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.
Two breaks from older integrations: old code does not work unchanged.
  1. The JWT TTL ceiling drops to 60 seconds: JWTs minted with exp = iat + 120 now return 401 AUTH_TOKEN_INVALID_CONFIG.
  2. Every 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: nonce_replay). Setting exp - iat > 60 seconds returns AUTH_TOKEN_INVALID_CONFIG. A missing X-Request-Signature header is likewise rejected with 401 AUTH_TOKEN_INVALID.

API key scopes

Selecting Write when you create an API key grants both read and write access, so you can also call GET endpoints with that key. Selecting Read grants read access only. POST /v3/fx/quotes requires read access even though it creates a quote; the other public POST operations require write access. A key without the required permission receives 403 AUTH_INSUFFICIENT_SCOPE.

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 these steps in order before signing. The server applies the same canonicalization to the request path.
  1. Empty path becomes /.
  2. Normalize percent-encoding per RFC 3986 §6.2.2: decode unreserved octets (%70 → p) and uppercase hex for reserved octets (%2f → %2F).
  3. Merge consecutive slashes (//a///b → /a/b).
  4. Remove dot-segments per RFC 3986 §5.2.4 (/a/./b/../c → /a/c). Encoded unreserved dots are normalized first, so /a/%2E/b becomes /a/b.
  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 setting; 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 for 80 seconds after the request signature verifies. An invalid signature does not consume the nonce. Reusing a reserved nonce returns 401 AUTH_TOKEN_INVALID with details.reason: nonce_replay.

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 and private key from the name/privateKey parameters in the API key JSON. For Postman, paste the script into your collection’s Pre-request Script, then set collection variables KEY_NAME and KEY_SECRET plus optional APP_MODE. Needs a Postman version with pm.require("npm:...") support.

Scopes

Two scopes apply, and they split on direction: A key without the scope a call needs returns 403 AUTH_INSUFFICIENT_SCOPE. The Cheat sheet lists the scope for each operation.

Common mistakes

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

The per-org, per-environment limit, plus the headers to watch.