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...
ComponentDescription
tRequest timestamp in epoch milliseconds
v1HMAC-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=a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1

2. 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);
}