X-OpenFX-Signature header, alongside the timestamp it was computed with. Verify both the signature and the timestamp’s age on every event before trusting the payload.
Signature header format
TheX-OpenFX-Signature header carries two comma-separated key=value fields: t, the Unix timestamp (seconds) the signature was computed at, and v1, the hex-encoded HMAC-SHA256 digest:
. separator before hashing. Including it in the signed payload (not just alongside it) means an attacker who captures a valid delivery can’t replay it later with a different t. The timestamp is authenticated, not just present.
Each delivery also carries an X-Trace-Id header containing an opaque trace identifier. Include this value when contacting support to help correlate deliveries in our logs.
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 each part on = to recover t and
v1. Reject the delivery if either field is missing.3
Reject stale timestamps
Reject if
|now - t| > 300 seconds (5 minutes). This bounds how long a
captured delivery remains replayable, so clock drift beyond a few minutes
between your server and OpenFX will start rejecting legitimate
deliveries. Keep your server clock synced (NTP).4
Compute the expected HMAC
Run
HMAC_SHA256("{t}.{raw_body}", 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 digest against
v1 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 both the timestamp and
signature check succeed.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 key comes from
- Live: download from the dashboard webhook settings.
- Sandbox: download separately from the Sandbox dashboard.
- Rotation: generate the new key, 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 key 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.
- 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 decoding. The
v1value is hex-encoded; do not base64-decode it. - Hashing the raw body alone. The signed payload is
{timestamp}.{raw_body}, not the raw body by itself. Reusing the pre-timestamp formula produces a digest that never matchesv1. - Skipping the timestamp check. Verifying
v1without checking|now - t| > 300accepts a captured delivery replayed at any point in the future. Both checks are required. - 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.
- Assuming signature verification also dedups redeliveries. It doesn’t. Each retry carries a fresh timestamp, so it produces its own valid signature rather than repeating the original. Dedup on the event
idseparately; see Common mistakes on the setup page.
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.Payout webhooks
Event shape for payout status changes.
Deposits
How deposits transition from pending to completed.
Withdrawals
How withdrawals move from PENDING to COMPLETED or CANCELLED.