Preserve bytes before parsing JSON
The signature covers exactly timestamp + '.' + raw_body, using HMAC SHA-256 and your environment’s webhook secret. Read the timestamp from X-Face-Auth-Timestamp and the signature from X-Face-Auth-Signature, in sha256=<hex> format.
Do not reconstruct the message using JSON.stringify or another serialization of the parsed object. Whitespace, line breaks or property ordering change the bytes. Configure your framework to expose the raw body before JSON parsing.
Backend verification
The example takes a time tolerance defined by your policy; it is not a delivery-time promise. Synchronize the server clock and assess redelivery together with your replay policy.
import { createHmac, timingSafeEqual } from "node:crypto";
// Read X-Face-Auth-Timestamp / X-Face-Auth-Signature. rawBody is the exact received Buffer.
export function verifyWebhook(rawBody, timestamp, signature, secret, maxAgeSeconds, nowSeconds = Date.now() / 1000) {
if (!Buffer.isBuffer(rawBody) || !secret || !Number.isFinite(maxAgeSeconds) || maxAgeSeconds <= 0 || !Number.isFinite(nowSeconds)) return false;
if (typeof timestamp !== "string" || !/^[0-9]{1,16}$/.test(timestamp) || typeof signature !== "string" || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
const sentAt = Number(timestamp);
if (!Number.isSafeInteger(sentAt) || Math.abs(nowSeconds - sentAt) > maxAgeSeconds) return false;
const expected = createHmac("sha256", secret).update(Buffer.from(timestamp + ".", "utf8")).update(rawBody).digest();
const received = Buffer.from(signature.slice(7), "hex");
return expected.length === received.length && timingSafeEqual(expected, received);
}
// Only after signature verification: parse and validate the envelope, compare X-Face-Auth-Event-Id with event_id, and durably accept with a unique event_id (atomic insert/queue). A duplicate event_id is acknowledged without repeating effects. Do not deduplicate transaction_id. GET the transaction to reconcile current status, decision and decision_reason; delivery order is not authoritative. Acknowledge only after durable acceptance. Persistence, queueing, body limits and business effects belong to your application; this function only verifies HMAC. maxAgeSeconds is YOUR replay policy, not a service delivery promise. Never reserialize JSON before verification or log secrets/payloads.
After verification
Only after verifying HMAC should you parse JSON, validate the envelope and process the event. Check the event identifier in the header against the body. Deduplicate by event_id using durable storage; cryptographic verification alone does not prevent repeating a business side effect.
These examples isolate verification and do not implement a complete receiver. Add body limits, exception handling, persistence, queueing, protected logging and reconciliation. Never log the secret, personal-data payloads or journey links in public tools.