DocsAPI ReferenceWebhook Signing & Verification

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.

Last updated:

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', 200

Verification 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?
On the buyer's detail page in the dashboard — the Webhook Signing Secret field next to the webhook URL. It's a 32-byte hex string unique per buyer. Store it as an environment variable on your receiver; never commit to source control. Treat it with the same care as an API key.
What HTTP header contains the webhook signature?
`X-LeadDistro-Signature`. The value is a lowercase hex-encoded HMAC-SHA256 digest computed over the raw JSON body bytes using the buyer's signing secret. Headers are case-insensitive per HTTP spec.
Do I have to verify webhook signatures, or is TLS enough?
TLS protects against in-transit tampering but not against an attacker who has guessed your webhook URL and is POSTing fake leads. Signature verification proves the payload came from Lead Distro AI specifically. For low-risk verticals signing verification is optional; for TCPA/HIPAA-adjacent verticals it's effectively required to satisfy compliance.
Can I rotate the signing secret without downtime?
Not natively today — rotation requires support intervention. For self-service rotation, schedule a brief maintenance window where you generate a new secret, update both Lead Distro AI's buyer config and the receiver's environment variable simultaneously, and verify a test send. Total downtime: ~5 minutes per buyer.
What happens if signature verification fails?
Reject the request with HTTP 401 and don't process the lead. Lead Distro AI's retry logic will not retry on 401 (treats it as a permanent error from the receiver's side) — instead, the buyer-side dashboard shows `delivery_failed` and you can investigate. Don't accept the lead 'just in case' — accepting unsigned payloads defeats the entire signing layer.
Why must I use a timing-safe comparison for signatures?
String comparison (`==` or `===`) returns false on the first byte mismatch — and the time taken depends on where the mismatch occurred. An attacker can measure response time to learn the expected signature one byte at a time. Timing-safe comparison takes constant time regardless of where bytes differ, preventing the side-channel leak.

If you have any questions, send us an email at support@leaddistro.ai