Skip to main content
Every webhook Fanvue sends carries a signature. Verify it on every request so an attacker cannot forge events against your endpoint or replay an old delivery. Reject anything that fails.

The signature header

Each delivery includes an X-Fanvue-Signature header:
X-Fanvue-Signature: t=<timestamp>,v0=<signature>
  • t is a Unix timestamp (seconds since epoch) recording when Fanvue signed the request.
  • v0 is the HMAC-SHA256 signature of the signed payload, encoded as a hexadecimal string.

Get your signing secret

The signature is computed with a per-app signing secret. Find it in the Fanvue Developer Area:
1

Open the Events tab

Go to the Fanvue Developer Area, select your app, and open the Events tab.
2

Reveal the secret

Click View Signing Secret and copy the value.
3

Store it server-side

Keep the secret in an environment variable (for example FANVUE_WEBHOOK_SECRET). Never ship it in client-side code or commit it to source control.

How verification works

Recompute the signature yourself and check it matches what Fanvue sent.
1

Parse the header

Split X-Fanvue-Signature on the comma and read t (the timestamp) and v0 (the signature). If either is missing, reject the request.
2

Reconstruct the signed payload

Concatenate the timestamp, a period, and the raw request body: {timestamp}.{body}. Use the exact bytes Fanvue sent. If you parse the JSON first and re-serialize it, the bytes change and the signature will not match.
3

Compute the expected signature

Run HMAC-SHA256 over the signed payload using your signing secret, then hex-encode the result.
4

Compare in constant time

Compare your computed signature against v0 with a timing-safe comparison (crypto.timingSafeEqual in Node, hmac.compare_digest in Python). A plain == can leak the secret through timing differences.
5

Reject stale timestamps

Check that t is within an acceptable window of the current time (5 minutes is a sensible default). This stops an attacker from replaying a previously valid request.
Verify against the raw request body, not a parsed-and-re-serialized version. Most frameworks expose the raw bytes through a hook (for example Express’s verify callback). Capture them before any JSON middleware mutates the request.

Code sample

Both samples take the raw request body and the X-Fanvue-Signature header value, and return whether the request is authentic.
import crypto from "crypto";

const SIGNING_SECRET = process.env.FANVUE_WEBHOOK_SECRET!;
const TOLERANCE_SECONDS = 300; // 5 minutes

export function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string
): boolean {
  // Parse "t=1234567890,v0=abc123..."
  let timestamp: string | undefined;
  let signature: string | undefined;

  for (const part of signatureHeader.split(",")) {
    const [key, value] = part.split("=");
    if (key === "t") timestamp = value;
    if (key === "v0") signature = value;
  }

  if (!timestamp || !signature) return false;

  // Reject stale timestamps to block replays
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > TOLERANCE_SECONDS) {
    return false;
  }

  // Recompute the signature over `{timestamp}.{rawBody}`
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(signedPayload)
    .digest("hex");

  // Constant-time comparison
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
When verification fails, respond with 401 and do not process the payload. When it succeeds, persist the event and return a 2xx as described in the Webhooks Overview.

Test it without waiting for a real event

Use the Test action in the webhook’s menu (in the Events & Endpoints list) to send a sample payload to your endpoint. It carries a real X-Fanvue-Signature header, so a passing test confirms your verification logic works end to end.