Verify signatures
HMAC-SHA256 signing scheme — verify before you trust an event.
Verify signatures
Every webhook is signed with your endpoint's whsec_… secret. Verify the signature before parsing the body.
Header
Spuke-Signature: t=1735689600,v1=6a76…f3
t— Unix timestamp of when spuke generated the signature.v1— HMAC-SHA256 of"{t}.{raw_body}"using yourwhsec_…secret, hex-encoded.
Steps
- Extract
tandv1from the header. - Reject if
|now - t| > 300seconds (replay protection). - Compute
expected = HMAC_SHA256(secret, t + "." + rawBody). - Compare
expectedwithv1in constant time.
Node.js example
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const t = Number(parts.t);
if (Math.abs(Date.now()/1000 - t) > 300) throw new Error("expired");
const expected = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex")
);
if (!ok) throw new Error("bad_signature");
}
PHP example
[$t, $v1] = [null, null];
foreach (explode(',', $_SERVER['HTTP_SPUKE_SIGNATURE']) as $p) {
[$k, $v] = explode('=', $p, 2);
if ($k === 't') $t = (int)$v;
if ($k === 'v1') $v1 = $v;
}
if (abs(time() - $t) > 300) http_response_code(400);
$expected = hash_hmac('sha256', $t.'.'.$raw, $secret);
if (!hash_equals($expected, $v1)) http_response_code(400);
Always use the raw request body — parsing to JSON first will change whitespace and invalidate the signature.