Skip to main content
Every webhook delivery is signed with HMAC-SHA256 using your organization’s webhook signing secret. The signature is delivered in the X-OpenFX-Signature header. Verify the signature — and the timestamp it carries — on every event before trusting the payload.

Signature header format

The X-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).
The signed payload is computed by concatenating the timestamp, a literal ., and the raw request body:
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. 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.
Sandbox and Live secrets are different. A handler using the wrong environment’s secret will reject every event. Wire the secret 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.
  • 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. 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 encoding. The v1 signature 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.