Signature Verification
Every webhook request includes an x-flex-signature header that allows you to verify the request originated from Flex and has not been tampered with.
Header Format
x-flex-signature: t=1713168600000,v1=a3f2b8c...| Component | Description |
|---|---|
t | Request timestamp in epoch milliseconds |
v1 | HMAC-SHA256 signature of the request |
Verification Steps
1. Parse the header
Extract the t and v1 values from the x-flex-signature header.
t=1713168600000,v1=a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a12. Check the timestamp
Compare t against the current time. Reject the request if the timestamp falls outside an acceptable tolerance window (5 to 10 minutes is recommended) to guard against replay attacks.
const toleranceMs = 5 * 60 * 1000; // 5 minutes
const nowMs = Date.now();
if (Math.abs(nowMs - Number(t)) > toleranceMs) {
// Reject - possible replay attack
}3. Compute the expected signature
Build the signing input by concatenating the timestamp, the full request URL, and the raw request body. Compute the HMAC-SHA256 using the webhook secret configured for your application.
signing_input = {t} + {url} + {body}
expected = HMAC-SHA256(signing_input, secret)Example values:
t = 1713168600000
url = https://api.example.com/webhooks/flex
body = {"id":"evt_abc123","date":"2026-04-15T08:30:00Z","field1": "..."}
secret = whsec_S3cr3tK3y
signing_input = 1713168600000https://api.example.com/webhooks/flex{"id":"evt_abc123","date":"2026-04-15T08:30:00Z","field1": "..."}4. Compare signatures
Use a constant-time comparison to check the computed signature against the v1 value from the header. A standard string equality check is vulnerable to timing attacks.
import { timingSafeEqual } from "node:crypto";
const expectedBuf = Buffer.from(expected, "hex");
const receivedBuf = Buffer.from(v1, "hex");
if (!timingSafeEqual(expectedBuf, receivedBuf)) {
// Reject - signature mismatch
}Full Example (NodeJs)
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(url, rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=", 2))
);
const { t, v1 } = parts;
// Check timestamp tolerance (5 minutes)
const toleranceMs = 5 * 60 * 1000;
if (Math.abs(Date.now() - Number(t)) > toleranceMs) {
return false;
}
// Compute expected signature
const signingInput = t + url + rawBody;
const expected = createHmac("sha256", secret)
.update(signingInput)
.digest("hex");
// Constant-time comparison
const expectedBuf = Buffer.from(expected, "hex");
const receivedBuf = Buffer.from(v1, "hex");
if (expectedBuf.length !== receivedBuf.length) {
return false;
}
return timingSafeEqual(expectedBuf, receivedBuf);
}Updated 7 days ago