Signature Verification

Every webhook request includes a x-flex-signature header that allows you to verify the request originated from Flex and has not been tampered with. Flex uses HMAC to sign requests. See the Verification Flow below for more information on how to validate requests once you have generated a secret key.

Generating a Secret Key

Head over to your Flex instance, and click "Manage" at the top right. This is where you can access your configuration settings for Flex. If you don't see this link, please reach out to the Flex team to ensure your profile has the appropriate permissions to manage these settings.

In the management area, you will be able to create and manage webhooks. When creating a new webhook, you will be presented with a unique secret key for the webhook instance. This should be treated as an application secret. This will only be displayed once, so please copy it and store it appropriately.

If you need to deactivate (temporarily) or delete (permanently) a webhook, you can do so through the management interface.

If you need to rotate a token, click "View" and "Rotate Token". Once rotated, new webhook requests will be signed using the new token. To manage the transition, you might have a short period where your verification flow fails as the new token is yet to be configured. In this instance, you can rely on Flex's retry logic to keep retrying until such time you have updated your application with the new token.

Verification Flow

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