A webhook sends each new lead's attribution to a URL you choose, the moment TraceLead records it: Zapier, Make, n8n, a Slack channel or your own endpoint. It carries the same attribution the dashboard shows. It never carries the visitor's answers, contact details or any visitor or session identifier.
- Adding a webhook
- The request
- Verifying the signature
- Sending leads to Slack
- Delivery and retries
- What is never sent
Adding a webhook
On your site's page in TraceLead, open Reports & data → Lead webhooks, choose Send as (Signed JSON for Zapier, Make or your own endpoint, Slack message for a Slack channel), paste an https:// URL and choose Add webhook. For signed JSON, copy the signing secret that appears: it is shown once. A site can have up to three webhooks. Send test event posts a sample lead with "test": true and shows the result in the delivery log.
Each webhook receives only the events it is set to receive: New lead (lead.created, ticked by default) and Contact click (contact_click.created, for sites that record contact clicks). Tick or untick them on the webhook at any time; a webhook never receives an event you did not tick.
Only owners and admins can add, test or remove webhooks. Endpoints must be public: TraceLead refuses URLs that resolve to private, loopback or link-local addresses, with credentials in the URL, or without https.
The request
POST with Content-Type: application/json:
{
"event": "lead.created",
"event_id": 48213,
"site_public_id": "tl_abcdefghij1234567890",
"occurred_at": "2026-10-12T09:41:07Z",
"form_id": "contact",
"page_path": "/contact",
"channel": "paid_search",
"source": "google",
"medium": "cpc",
"utm_campaign": "autumn-sale",
"utm_term": "lead attribution",
"utm_content": null,
"utm_id": null,
"click_id_type": "gclid",
"referrer_host": "www.google.com",
"landing_page": "/pricing?utm_campaign=autumn-sale",
"first_touch_at": "2026-10-09T15:02:44Z",
"test": false
}
click_id_type names the click id family (gclid, msclkid, …), never its value. event_id is unique per lead, so you can ignore a repeated delivery.
Headers:
| Header | Value |
|---|---|
X-TraceLead-Event |
lead.created or contact_click.created |
X-TraceLead-Timestamp |
Unix time in seconds when the request was signed |
X-TraceLead-Signature |
sha256= followed by the hex HMAC-SHA256 of {timestamp}.{body} with your signing secret |
Contact click events
contact_click.created has the same shape with kind (phone, email, sms or whatsapp) in place of form_id, and page_path is the page where the link was clicked:
{
"event": "contact_click.created",
"event_id": 913,
"site_public_id": "tl_abcdefghij1234567890",
"occurred_at": "2026-10-12T09:44:12Z",
"kind": "phone",
"page_path": "/contact",
"channel": "paid_search",
"source": "google",
"medium": "cpc",
"utm_campaign": "autumn-sale",
"utm_term": null,
"utm_content": null,
"utm_id": null,
"click_id_type": "gclid",
"referrer_host": "www.google.com",
"landing_page": "/pricing?utm_campaign=autumn-sale",
"first_touch_at": "2026-10-09T15:02:44Z",
"test": false
}
The number, address or message of the link is never part of it: TraceLead never receives them. event_id is unique per contact click, and counts separately from lead ids.
Verifying the signature
Recompute the HMAC over the timestamp, a dot and the raw request body, compare in constant time, and reject requests whose timestamp is more than five minutes old.
$secret = getenv('TRACELEAD_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TRACELEAD_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_TRACELEAD_SIGNATURE'] ?? '';
$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
if (! hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit;
}
$lead = json_decode($body, true);
http_response_code(204);
import crypto from 'node:crypto';
export function verify(rawBody, headers, secret) {
const timestamp = headers['x-tracelead-timestamp'];
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const given = headers['x-tracelead-signature'] ?? '';
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
return fresh && given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
Zapier's "Catch Hook" and Make's custom webhook accept the request as is; verify the signature in a code step if the destination matters.
Sending leads to Slack
- In Slack, create an app for your workspace at api.slack.com/apps (From scratch), open Incoming Webhooks, switch them on and choose Add New Webhook to Workspace. Pick the channel the leads should go to.
- Copy the webhook URL. It starts with
https://hooks.slack.com/services/; TraceLead accepts no other address for the Slack format. - In TraceLead, add a webhook with Send as: Slack message and paste the URL. Choose Send test event: a "Test lead on" message with your site's name should appear in the channel.
Each lead is posted as one message: the channel, source / medium, campaign, landing page, the page the form was on, the form id, and when the lead and its first touch happened. These are the same columns as the JSON body, with the same exclusions below.
Slack messages are not signed (Slack cannot check a signature), and the webhook URL itself is the credential, so TraceLead shows only its first part again after you add it. Anyone with the URL can post to that channel; if it leaks, remove the webhook in Slack and add a new one. Values that came from a visitor, such as a campaign name, are escaped so they cannot become links or @channel mentions.
Delivery and retries
- Answer with any
2xxstatus within 5 seconds. Redirects are not followed. - A failed delivery is retried three times, after about 10 seconds, 1 minute and 5 minutes.
- After 50 failed attempts in a row the webhook is switched off and the organisation's owners get an email. Switch it back on from the site page once the endpoint works; the counter resets.
- The site page shows the last 20 attempts per webhook with their status code and timing. Request and response bodies are not stored.
What is never sent
Names, email addresses, phone numbers, form answers, IP addresses, user agents, full referrer URLs, click id values, and the random visitor and session ids TraceLead uses for counting. See What is sent to TraceLead for how the attribution is collected.