Webhook Signing & Verification
Verify Lead Distro AI webhook payloads with HMAC-SHA256 signatures. Signing secret format, X-LeadDistro-Signature header, and verification recipes in Node, Python, PHP.
Why Sign Webhooks?
Lead Distro AI signs every outbound webhook with HMAC-SHA256 over the JSON body. Receivers can verify the signature to confirm: (1) the payload originated from Lead Distro AI (not a spoofer), and (2) the body wasn't tampered with in transit (TLS already protects against this, but signature gives belt-and-suspenders assurance).
Required for TCPA-sensitive verticals (legal lead distribution), HIPAA-adjacent leads, and any vertical where regulators may ask 'how do you know that lead came from your verified source?'
How the Signature Is Computed
- Take the raw JSON body bytes of the webhook POST.
- Compute HMAC-SHA256 with the buyer-specific signing secret as the key.
- Encode the resulting 32-byte digest as a lowercase hex string.
- Include it in the `X-LeadDistro-Signature` HTTP header on the webhook request.
Finding the Signing Secret
- Open the buyer's detail page in Lead Distro AI.
- Find the Webhook Signing Secret field (alongside the webhook URL).
- Copy the secret — it's a 32-byte hex string.
- Store it as an environment variable on the receiver's side. Don't commit to source control.
Verification Recipe — Node.js
import crypto from 'crypto';
function verifyLeadDistroSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
// In Express:
app.post('/webhooks/leaddistro', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.get('x-leaddistro-signature');
const secret = process.env.LEADDISTRO_SIGNING_SECRET;
if (!verifyLeadDistroSignature(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}
const lead = JSON.parse(req.body.toString());
// process lead
res.status(200).send('OK');
});Verification Recipe — Python
import hmac, hashlib
def verify_leaddistro_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# In Flask:
@app.route('/webhooks/leaddistro', methods=['POST'])
def leaddistro_webhook():
signature = request.headers.get('X-LeadDistro-Signature', '')
secret = os.environ['LEADDISTRO_SIGNING_SECRET']
if not verify_leaddistro_signature(request.data, signature, secret):
return 'Invalid signature', 401
lead = request.get_json()
# process lead
return 'OK', 200Verification Recipe — PHP
<?php
function verify_leaddistro_signature($raw_body, $signature, $secret) {
$expected = hash_hmac('sha256', $raw_body, $secret);
return hash_equals($expected, $signature);
}
$raw_body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_LEADDISTRO_SIGNATURE'] ?? '';
$secret = getenv('LEADDISTRO_SIGNING_SECRET');
if (!verify_leaddistro_signature($raw_body, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
$lead = json_decode($raw_body, true);
// process lead
http_response_code(200);
echo 'OK';Always use a **timing-safe comparison** (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python, `hash_equals` in PHP) when comparing signatures. Naive string comparison leaks the expected signature one byte at a time to timing-attack adversaries.
Frequently Asked Questions
Where do I find the Lead Distro AI webhook signing secret?
What HTTP header contains the webhook signature?
Do I have to verify webhook signatures, or is TLS enough?
Can I rotate the signing secret without downtime?
What happens if signature verification fails?
Why must I use a timing-safe comparison for signatures?
Related Articles
If you have any questions, send us an email at support@leaddistro.ai