Verifying webhook signatures

How to validate the X-LeadPilot-Signature header over the exact raw request body.

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

Save the signing secret when you create the subscription. Later responses mask it. To replace a secret, create a replacement subscription. Update does not rotate the secret.

Verify in Node.js

import crypto from 'node:crypto';

export function verify(req, secret) {
  const header = req.headers['x-leadpilot-signature'] || '';
  if (typeof header !== 'string' || !/^sha256=[0-9a-f]{64}$/.test(header)) return false;
  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.

See the developer guide for the complete delivery contract.

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.