Webhooks
Signatures
Verify that a webhook came from Q4.
Every delivery has an X-Q4-Signature header:
X-Q4-Signature: t=1762228931,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdv1 is an HMAC-SHA256 of {t}.{raw body}, keyed with your webhook's signing_secret.
- Split the header on
,to gettandv1. - Reject it if
tis more than 5 minutes old. - Compute the HMAC of
t + "." + bodyusing the raw request body, before any JSON parsing. - Compare it to
v1in constant time.
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + "." + rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(
secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])