# How LeadTap.me webhooks work

> LeadTap.me has two kinds of webhook. The CRM webhook sends every lead in the account to your CRM, an AI agent workflow, Zapier, Make, n8n or your sales team; a page webhook sends one lead page's submissions. JSON, signed with HMAC-SHA256, off until the account owner turns it on. A Plus feature.

Source: https://docs.leadtap.me/concepts/webhooks/ · Last updated: 2026-10-09

A LeadTap.me webhook sends lead data, as JSON, to a URL the customer chooses. There are two kinds: the **CRM webhook** sends every lead in the account, whatever page they came from, and a **page webhook** sends the submissions of one lead page. Webhooks are a Plus feature, and a webhook sends nothing until the account owner turns it on in the portal.

## CRM webhook or page webhook: which one to use

Use the CRM webhook to get people into a CRM, an AI agent workflow or in front of a sales team; use a page webhook for one page's data.

| | CRM webhook | Page webhook |
|---|---|---|
| Scope | The whole account: every lead page | One lead page |
| What it sends | A person (the lead) | A submission of that page |
| Events | `lead.created`, `lead.verified`, `lead.submission` | `complete`, `partial` |
| Use it for | A CRM, an AI agent workflow, Zapier, Make, n8n, a sales team | Statistics, a spreadsheet, a record of what the page collected |
| Turned on in the portal at | Integrations | The lead page editor's Connect tab |
| MCP tools | `list_crm_webhooks`, `create_crm_webhook`, `update_crm_webhook` | `list_page_webhooks`, `create_page_webhook`, `update_page_webhook` |

Both kinds share `test_webhook`, `list_webhook_deliveries` and `retry_webhook_deliveries`.

## Connecting LeadTap.me to HubSpot, Google Sheets and other tools

LeadTap.me sends leads to other tools only through webhooks; it has no direct HubSpot or Google Sheets integration. To get every lead into HubSpot or another CRM, point the CRM webhook at Zapier, Make or n8n and let it pass each person on. To fill a spreadsheet with one page's answers, point that page's webhook at the sheet's webhook URL or at Zapier, Make or n8n.

## How a LeadTap.me webhook is set up

A LeadTap.me webhook of either kind starts off and only the account owner turns it on.

1. The webhook is added in the portal, or by Claude with `create_crm_webhook` or `create_page_webhook`.
2. It starts **off**. The account owner gets an email that says what it is for (the CRM, meaning every lead in the account, or the lead page), the webhook's host (never the full URL) and its events, with a button to turn it on and a link to revoke the connected app's access if the owner did not ask for the webhook.
3. The owner turns it on in the portal: under Integrations for the CRM webhook, in the lead page editor's Connect tab for a page webhook. Only then does it send lead data.

Claude can create a webhook, change its events, point it at a new URL, turn it off, test it and read its delivery log, but it cannot turn a webhook on or delete it. Pointing a webhook at a new URL turns it off again and emails the owner, like creating one.

## Webhook URL rules

A LeadTap.me webhook URL must be `https`, on a public host, with no credentials in it. Claude asks the customer for the URL and never guesses it. After the webhook is created, LeadTap.me shows only its host, never the full URL or the signing secret.

## CRM webhook events

A LeadTap.me CRM webhook fires on the moments in a person's life in the account.

| Event | When it fires |
|---|---|
| `lead.created` | A person appears in the account for the first time, from any lead page (the default) |
| `lead.verified` | The person confirms their email |
| `lead.submission` | A person the account already knows answers again; what they answered enriches them |

## Page webhook events

A LeadTap.me page webhook fires on one or both of two events of its lead page.

| Event | When it fires |
|---|---|
| `complete` | Someone finished the lead page (the default) |
| `partial` | Someone left the lead page halfway. Partial submissions must be on for the page |

## Webhook request headers

Every LeadTap.me webhook request, of either kind, is an HTTP `POST` with a JSON body and these headers.

| Header | Value |
|---|---|
| `X-Leadtap-Event` | The event: `lead.created`, `lead.verified`, `lead.submission`, `complete` or `partial` |
| `X-Leadtap-Idempotency-Key` | The same key on every retry of one delivery. Use it to drop duplicates |
| `X-Leadtap-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>`, only when the webhook has a secret |
| `X-Leadtap-Test` | `1`, only on test sends |

## CRM webhook payload

The body of a LeadTap.me CRM webhook is about a person: who they are in `lead`, and where they came from and what they answered in `custom_data`. Answers are keyed by the step key of each question, not by its wording, so an integration does not break when a question is reworded.

| Field | What it holds |
|---|---|
| `event` | `lead.created`, `lead.verified` or `lead.submission` |
| `test` | `true` on test sends; absent otherwise |
| `occurred_at` | When it happened, ISO 8601 |
| `lead` | `email`, `first_name`, `last_name`, `phone`, `verified` (whether they confirmed their email) |
| `custom_data` | `form_id`, `form_name`, `form_slug`, `submission_id`, `session_id`, `score`, `score_max`, `outcome`, and the answers by step key |
| `custom_data_labels` | The question wording, by step key |
| `utm` | The UTM parameters of the visit |

## Page webhook payload

The body of a LeadTap.me page webhook is one submission of its lead page. Answers are keyed by step key; `labels` carries the wording for people to read.

| Field | What it holds |
|---|---|
| `event` | `complete` or `partial` |
| `test` | `true` on test sends; absent otherwise |
| `occurred_at` | When it happened, ISO 8601 |
| `submission_id`, `session_id` | Ids of the submission and the visit |
| `form` | `id`, `name`, `slug` of the lead page |
| `contact` | `email`, `first_name`, `last_name`, `phone`, `verified` |
| `score` | `value`, `max` and `outcome` |
| `answers` | The answers, by step key |
| `labels` | The question wording, by step key |
| `utm` | The UTM parameters of the visit |

## How to verify the webhook signature

A LeadTap.me webhook with a secret is signed with HMAC-SHA256 over `<t>.<raw body>`, where `t` is the Unix timestamp in the header. To verify it, recompute the HMAC with your secret over the exact bytes received, compare it to `v1` in constant time, and reject a `t` more than 300 seconds (5 minutes) from your clock. It works the same for both kinds of webhook.

```js
import crypto from "node:crypto";

// rawBody: the request body exactly as received (a string), not re-serialized JSON.
export function isFromLeadTap(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.trim().split("=")));
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

The secret is the customer's own; LeadTap.me never returns it once it is set.

## Deliveries and retries

LeadTap.me waits up to 10 seconds for the receiver, and the answer decides what happens next.

| Receiver answers | Result |
|---|---|
| `2xx` | Delivered |
| `408`, `429`, `5xx`, a network error or a timeout | Retried later |
| Any other `4xx` | Failed, not retried: the receiver rejected the request |

A delivery can arrive more than once, so receivers should drop repeats by `X-Leadtap-Idempotency-Key`. Every delivery is recorded in the webhook's delivery log with its status (`pending`, `delivered` or `failed`), attempts, HTTP status, the receiver's answer and the last error. Failed and stuck deliveries can be retried from the portal or with `retry_webhook_deliveries`.

## Testing a webhook

A test send posts one sample event right away, even if the webhook is off: a sample `lead.created` for a CRM webhook, or a sample submission built from the page's real questions for a page webhook. It carries `"test": true` and `X-Leadtap-Test: 1`, and it leaves no trace in the delivery log. Test after creating a webhook or changing its URL, before the owner turns it on.

## LeadTap.me webhook tools for Claude

Through the [MCP server](https://docs.leadtap.me/mcp/), Claude manages webhooks with nine tools. Full descriptions and parameters are in the [tool catalog](https://docs.leadtap.me/mcp/tools/#webhooks-tools).

| Tool | What it does |
|---|---|
| `list_crm_webhooks` | The account's CRM webhooks, their events and whether each one is on |
| `create_crm_webhook` | Adds a CRM webhook to the account; it starts off |
| `update_crm_webhook` | Turns a CRM webhook off, changes its events or points it at a new URL |
| `list_page_webhooks` | One lead page's webhooks, their events and whether each one is on |
| `create_page_webhook` | Adds a webhook to one lead page; it starts off |
| `update_page_webhook` | Turns a page webhook off, changes its events or points it at a new URL |
| `test_webhook` | Sends one sample event to a webhook of either kind, now |
| `list_webhook_deliveries` | A webhook's delivery log, newest first |
| `retry_webhook_deliveries` | Re-queues a webhook's failed and stuck deliveries and delivers them now |

In the MCP tools, a webhook's id is its `destinationId`: the list tools return it, and the other webhook tools take it.
