Verify webhook signatures
Verify the exact timestamp-and-body HMAC contract in Node.js, Python, and PHP.
Last updated
Quire signs each attempt with HMAC-SHA256. Read the raw request body bytes before JSON parsing, then compute:
timestamp + "." + rawBody
Compare the hexadecimal digest to X-Quire-Signature-256 after its sha256= prefix using a constant-time comparison. Use X-Quire-Timestamp as the timestamp string. Reject stale timestamps according to your risk tolerance to reduce replay risk.
Headers
Every attempt sends:
X-Quire-EventX-Quire-Event-IdX-Quire-Delivery-IdX-Quire-AttemptX-Quire-TimestampX-Quire-Signature-256
Node.js and TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyQuire(
rawBody: Buffer,
timestamp: string,
signatureHeader: string,
secret: string,
): boolean {
const expected = createHmac("sha256", secret)
.update(timestamp)
.update(".")
.update(rawBody)
.digest();
const providedHex = signatureHeader.replace(/^sha256=/, "");
if (!/^[a-f0-9]{64}$/i.test(providedHex)) return false;
return timingSafeEqual(expected, Buffer.from(providedHex, "hex"));
}
Python
import hashlib
import hmac
def verify_quire(raw_body: bytes, timestamp: str, header: str, secret: str) -> bool:
signed = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
provided = header.removeprefix("sha256=")
return hmac.compare_digest(expected, provided)
PHP
<?php
function verifyQuire(string $rawBody, string $timestamp, string $header, string $secret): bool
{
$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);
$provided = preg_replace('/^sha256=/', '', $header) ?? '';
return hash_equals($expected, $provided);
}
Re-encoding parsed JSON changes bytes and breaks verification. Capture the framework's raw body exactly as received.