Skip to main content
Every webhook delivery is signed with HMAC-SHA256 over a timestamp-prefixed payload, using your organization’s webhook signing secret. The signature is delivered in the 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

The X-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:
The timestamp is concatenated with the raw body using a literal . 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.
Sandbox and Live signing keys are different. A handler using the wrong environment’s key will reject every event. Wire the key from environment-aware config, not a hard-coded constant.

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. Use crypto.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 v1 value 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 matches v1.
  • Skipping the timestamp check. Verifying v1 without checking |now - t| > 300 accepts 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 id separately; 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.