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 your whsec_… secret, hex-encoded.

Steps

  1. Extract t and v1 from the header.
  2. Reject if |now - t| > 300 seconds (replay protection).
  3. Compute expected = HMAC_SHA256(secret, t + "." + rawBody).
  4. Compare expected with v1 in 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.