X-OpenFX-Signature header. Verify the signature — and the timestamp it carries — on every event before trusting the payload.
Signature header format
TheX-OpenFX-Signature header has the following format:
t— Unix timestamp (seconds) of the delivery. Verify this is within an acceptable window (5 minutes is recommended) to prevent replay attacks.v1— hex-encoded HMAC-SHA256 of the signed payload (see below).
., and the raw request body:
X-Trace-Id header containing an opaque trace identifier. Include this value when contacting support to help correlate deliveries in our logs.
The backend additionally sends a legacy X-REDENVELOPE-SIGNATURE header containing an HMAC computed with the same signing secret. It is a deprecated alias kept for backward compatibility — verify X-OpenFX-Signature instead.
How verification works
1
Capture the raw request body
Read the body as the exact bytes you received it. Do not parse-then-reserialize JSON — whitespace and key ordering matter, and a re-serialized body will not match the signature.
2
Parse the signature header
Split
X-OpenFX-Signature on , and extract the t and v1 values. Reject the delivery immediately if either is absent.3
Check the replay window
Compare the
t timestamp against the current time. Reject deliveries where |now - t| > 300 seconds (5 minutes). This prevents replayed deliveries from being accepted.4
Compute the expected HMAC
Build the signed payload as
t + "." + raw_body. Run HMAC_SHA256(signed_payload, signing_secret) and hex-encode the digest. The signing secret is the value labeled Signing key in your dashboard webhook settings.5
Compare in constant time
Compare your computed HMAC against the
v1 value from the header using a constant-time comparator. Naive == / === comparison is timing-attack-vulnerable.6
Reject on mismatch
Respond with
401 Unauthorized (or 400 Bad Request) and do not call your handler. Never trust the payload before verification succeeds.Verification samples
Reading the raw body: every framework that does JSON body parsing before your handler runs is a footgun. In Express, use
express.raw({ type: 'application/json' }); in Flask, request.get_data() before any .get_json(); in Go, read r.Body before the framework consumes it. The raw bytes are not optional.Where the signing secret comes from
- Live: download from the dashboard webhook settings. The signing secret has no environment prefix — it begins with
whsec_(e.g.whsec_...). - Sandbox: download separately from the Sandbox dashboard. Sandbox secrets carry a
sandbox_prefix (e.g.sandbox_whsec_...). - Rotation: generate the new secret, deploy your verifier to accept both old and new for at least one delivery cycle (allowing in-flight deliveries to clear), then revoke the old. Treat the secret like a Live credential — store in a secrets manager, never check into source.
Additional delivery headers
Common mistakes
- Parsing then re-serializing the body before computing the HMAC. Whitespace and key ordering differ; the signature will never match. Capture the raw bytes first.
- Skipping the replay-window check. Verifying only the HMAC without checking the timestamp leaves your handler open to replay attacks. Check
|now - t| <= 300. - Using
==/===for comparison. Timing-attack-vulnerable. Usecrypto.timingSafeEqual(Node),hmac.compare_digest(Python),hmac.Equal(Go),MessageDigest.isEqual(Java), a manually constant-time byte comparison (C++, which has no built-in one). - Hex vs base64 encoding. The
v1signature value is hex-encoded; do not base64-decode it. - Confusing the API key with the webhook signing secret. They are two different secrets, both downloadable from the dashboard. The webhook signing secret does not appear in any API request — it only signs deliveries to you.
- Trusting payload contents before signature verification. Reject unsigned or mis-signed events with a 401 before reading any field of the body.
What’s next
Deposit webhooks
Event shape for fiat and stablecoin deposits.
Withdrawal webhooks
Event shape for withdrawals, including the typed
settlement receipt on
completed wires.Deposit lifecycle
How deposits transition from pending to completed.
Withdrawal lifecycle
How withdrawals move through processing.