{/* Generated by scripts/docs/generate-webhook-docs.ts from the verifiers that the signature vectors test. Do not edit. */}
Each request carries X-RankPilot-Signature: t=<unix>,v1=<hex>. The signed string is <t>.<raw request body>, the key is your endpoint secret (whsec_…) and the algorithm is HMAC-SHA256. While a secret is being rotated the header holds two v1 values; accept the request if either matches. Compare in constant time and reject a timestamp more than 300 seconds from your clock. Verify the raw body, before any JSON parsing.
Node
// Reference verifier for a webhook receiver (S22.10). Standalone on purpose: the docs embed this file as it is, so a reader
// can paste it without our packages. Header: `t=<unix>,v1=<hex>[,v1=<hex>]`; signed string: `${t}.${rawBody}`.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function verifyWebhook(
secret: string,
header: string,
rawBody: string,
now = Math.floor(Date.now() / 1000),
): boolean {
const parts = header.split(',').map((p) => p.split('=') as [string, string | undefined]);
const stamp = Number(parts.find(([k]) => k === 't')?.[1]);
if (!Number.isFinite(stamp) || Math.abs(now - stamp) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret).update(`${stamp}.${rawBody}`).digest();
return parts
.filter(([k]) => k === 'v1')
.some(([, v]) => {
if (v === undefined || !/^[0-9a-f]{64}$/i.test(v)) return false;
return timingSafeEqual(Buffer.from(v, 'hex'), expected);
});
}
Python
# Reference verifier for a webhook receiver (S22.10). Header: t=<unix>,v1=<hex>[,v1=<hex>]; signed string: "<t>.<rawBody>".
import hashlib
import hmac
import re
import time
TOLERANCE_SECONDS = 300
def verify_webhook(secret: str, header: str, raw_body: str, now=None) -> bool:
now = int(time.time()) if now is None else now
parts = [p.split("=", 1) for p in header.split(",") if "=" in p]
stamps = [v for k, v in parts if k == "t"]
if not stamps or not re.fullmatch(r"\d+", stamps[0]):
return False
t = int(stamps[0])
if abs(now - t) > TOLERANCE_SECONDS:
return False
expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return any(
re.fullmatch(r"[0-9a-fA-F]{64}", v) and hmac.compare_digest(v.lower(), expected)
for k, v in parts
if k == "v1"
)
PHP
<?php
// Reference verifier for a webhook receiver (S22.10). Header: t=<unix>,v1=<hex>[,v1=<hex>]; signed string: "<t>.<rawBody>".
const TOLERANCE_SECONDS = 300;
function verify_webhook(string $secret, string $header, string $rawBody, ?int $now = null): bool
{
$now ??= time();
$stamp = null;
$candidates = [];
foreach (explode(',', $header) as $part) {
$pair = explode('=', $part, 2);
if (count($pair) !== 2) {
continue;
}
if ($pair[0] === 't') {
$stamp ??= $pair[1];
} elseif ($pair[0] === 'v1') {
$candidates[] = $pair[1];
}
}
if ($stamp === null || !ctype_digit($stamp) || abs($now - (int) $stamp) > TOLERANCE_SECONDS) {
return false;
}
$expected = hash_hmac('sha256', $stamp . '.' . $rawBody, $secret);
foreach ($candidates as $candidate) {
if (preg_match('/^[0-9a-fA-F]{64}$/', $candidate) === 1 && hash_equals($expected, strtolower($candidate))) {
return true;
}
}
return false;
}