Verifying webhook signatures

How to validate the X-Funkel-Signature header so you trust what you receive.

Every webhook from Funkel carries an HMAC-SHA256 signature so your endpoint can prove the request came from us and wasn’t modified in flight. Verifying the signature is non-negotiable for anything that takes action on the payload.

Where the signature lives

  • Header name: X-LeadPilot-Signature
  • Format: sha256=<hex>
  • What it signs: the raw HTTP request body, byte-for-byte

Find your signing secret

Open Settings → Webhooks, click your subscription, and copy the Signing secret. The secret is shown once when the webhook is first created, store it somewhere safe. Treat it like a password; rotate it if it ever leaks.

Verify in Node.js

import crypto from 'node:crypto';

export function verify(req, secret) {
  const header = req.headers['x-leadpilot-signature'] || '';
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', secret)
          .update(req.rawBody)            // Buffer or string of the exact request body
          .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(header),
    Buffer.from(expected),
  );
}

Verify in Go

func verify(body []byte, header, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(header), []byte(expected))
}

Verify in Python

import hmac, hashlib

def verify(body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(header, expected)

Common mistakes

  • Signing the parsed JSON, not the raw body. Reformatting changes whitespace and breaks the signature. Most frameworks parse the body before you can hash it, capture the raw bytes first.
  • Using === or == to compare. Always use a constant-time comparison (timingSafeEqual, hmac.Equal, compare_digest) to avoid leaking secret bits via timing.
  • Hard-coding “Funkel” into the header check. The header is currently X-LeadPilot-Signature. Match on that name verbatim.

Test before you trust

On any webhook subscription, click Send test event in Settings → Webhooks. Funkel posts a synthetic payload signed with your real secret. Run your verifier against it; if it passes, you’re ready for live events.