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.