Skip to Content
GuidesWebhook Signature Verification

Webhook Signature Verification

Kaplaix signs every webhook delivery with HMAC-SHA256. Always verify signatures before processing webhook payloads to ensure they originate from Kaplaix and have not been tampered with.

Signature header

Each webhook request includes an x-kaplaix-signature header with this format:

t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
PartDescription
tUnix timestamp (seconds) when the signature was generated
v1HMAC-SHA256 hex digest

Verification steps

1. Parse the header

Split the header value by , and extract t (timestamp) and v1 (signature).

2. Build the signed payload

Concatenate the timestamp, a period (.), and the raw request body:

{timestamp}.{rawBody}

3. Compute the expected signature

HMAC-SHA256(secret, "{timestamp}.{rawBody}")

4. Compare signatures

Use a timing-safe comparison to prevent timing attacks.

5. Check the timestamp

Reject signatures older than 5 minutes to prevent replay attacks.

Implementation examples

TypeScript / Node.js

import { createHmac, timingSafeEqual } from "node:crypto"; function verifyWebhookSignature( rawBody: string, signatureHeader: string, secret: string, toleranceSeconds = 300, ): boolean { // 1. Parse header const parts = Object.fromEntries( signatureHeader.split(",").map((p) => { const [k, ...v] = p.split("="); return [k, v.join("=")]; }), ); const timestamp = parts["t"]; const signature = parts["v1"]; if (!timestamp || !signature) return false; // 2. Check timestamp (replay protection) const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10); if (Math.abs(age) > toleranceSeconds) return false; // 3. Compute expected signature const signedPayload = `${timestamp}.${rawBody}`; const expected = createHmac("sha256", secret) .update(signedPayload) .digest("hex"); // 4. Timing-safe comparison const a = Buffer.from(signature, "hex"); const b = Buffer.from(expected, "hex"); if (a.length !== b.length) return false; return timingSafeEqual(a, b); }

Python

import hashlib import hmac import time def verify_webhook_signature( raw_body: str, signature_header: str, secret: str, tolerance_seconds: int = 300, ) -> bool: # 1. Parse header parts = dict(p.split("=", 1) for p in signature_header.split(",")) timestamp = parts.get("t") signature = parts.get("v1") if not timestamp or not signature: return False # 2. Check timestamp (replay protection) age = abs(int(time.time()) - int(timestamp)) if age > tolerance_seconds: return False # 3. Compute expected signature signed_payload = f"{timestamp}.{raw_body}" expected = hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest() # 4. Timing-safe comparison return hmac.compare_digest(signature, expected)

Security considerations

  • Always verify — never process unverified webhooks in production
  • Use timing-safe comparison — prevents timing side-channel attacks
  • Enforce timestamp tolerance — 5 minutes (300 seconds) is recommended
  • Store secrets securely — the webhook secret is shown once at channel creation; treat it like an API key
  • Rotate on compromise — use POST /v1/notification-channels/:id/rotate-secret if the secret is exposed
Last updated on