Signature verification

Verify that a webhook came from Reventlov.

Every webhook request includes an HMAC-SHA256 signature header:

1X-Reventlov-Signature: t=1712918400,v1=8b7f…
  • t is the Unix timestamp when we signed
  • v1 is hmac_sha256(secret, t + "." + raw_body)

The secret is the value you received when creating the endpoint via POST /v1/webhook-endpoints. Store it somewhere you can reach from the webhook handler; we never show it again.

Verify in Node

1import { createHmac, timingSafeEqual } from 'node:crypto';
2
3export function verifySignature(header: string, raw: string, secret: string) {
4 const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
5 const t = Number(parts.t);
6 if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // reject replay
7 const expected = createHmac('sha256', secret).update(`${t}.${raw}`).digest('hex');
8 return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
9}

Verify in Python

1import hmac, hashlib, time
2
3def verify(header: str, raw: bytes, secret: str) -> bool:
4 parts = dict(p.split("=") for p in header.split(","))
5 t = int(parts["t"])
6 if abs(time.time() - t) > 300:
7 return False
8 expected = hmac.new(
9 secret.encode(), f"{t}.".encode() + raw, hashlib.sha256
10 ).hexdigest()
11 return hmac.compare_digest(expected, parts["v1"])

Rules

  • Always use the raw request body (bytes before any JSON parsing)
  • Reject timestamps older than 5 minutes — prevents replay attacks
  • Always use a timing-safe comparison
  • If the signature fails, respond 401 and do not process the event