Send leads to any webhook URL
For CRMs and tools that accept a JSON webhook.
Get your tool's webhook address
In your CRM or automation tool, create an incoming webhook and copy its address. It must be a public
https://address that accepts JSON.Add the webhook in VisitorLayer
Open your project's Webhooks, select New webhook, paste the address as the Endpoint URL, and choose your lead form. If your tool needs a token, select Add header, for example
Authorization: Bearer your-token.Start from this payload and reshape it to the fields your tool expects.
Payload{ "event": "{{event.name}}", "occurred_at": "{{event.occurred_at}}", "lead": { "email": "{{form.email}}", "name": "{{form.name}}", "phone": "{{form.phone}}" }, "channel": "{{attribution.channel}}", "first_touch_channel": "{{first_touch.channel}}", "utm_source": "{{attribution.utm_source}}", "utm_campaign": "{{attribution.utm_campaign}}", "page": "{{page.url}}" }Rename each
form.token to match your form's fields, or pick them from Insert field. A field your form does not capture sendsnull.{{attribution.channel}}sends the same channel the Channels page shows for the lead, including your own channel rules.Send a test lead
Save the webhook and select Test webhook. Your tool receives one request with example values or your latest lead. Then submit your form on the live site.
What each request looks like
Each lead is its own POST request, sent shortly after the form is submitted. The body is exactly your payload, and these headers come with it:
| Content-Type | application/json |
|---|---|
| X-VisitorLayer-Delivery-Id | The same on every retry of one delivery |
| X-VisitorLayer-Timestamp | Unix seconds when the request was signed |
| X-VisitorLayer-Signature | v1= followed by a hex HMAC-SHA256 |
Answer with any 2xx status within 10 seconds. Timeouts, 408, 429, and 5xx answers are retried up to five attempts over about two and a half hours. Redirects, 401, 403, and 404 fail without a retry. A lead can arrive twice, so use the delivery ID to ignore repeats.
Check the signature
If you write your own receiver, check that a request came from VisitorLayer. The signature is an HMAC-SHA256, keyed with the webhook's signing secret, over the timestamp, a dot, and the raw body.
import { createHmac, timingSafeEqual } from "node:crypto";
// secret: the webhook's signing secret. body: the raw request body, unparsed.
function isFromVisitorLayer(secret, timestamp, body, signature) {
const expected = "v1=" + createHmac("sha256", secret).update(timestamp + "." + body).digest("hex");
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
return age < 300 && expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}