Send new leads to n8n
Receive each new lead in n8n with the raw body, check the signature in a Code node, and pass the lead to your CRM.
n8n receives a Zapped lead in a Webhook node. With the raw body kept and the signature checked in a Code node, the workflow can prove the request came from Zapped before it creates anything.
Use this guide whenYou run n8n, on n8n Cloud or on your own server, and you want new leads from Zapped to arrive in your own workflow.
Before you start
- Professional includes lead webhooks. In your Personal workspace, open Lead webhooks to configure your first destination. If a previously revoked destination is blocked, contact support to restore access.
- If you self host n8n, it must be reachable from the internet on HTTPS port 443. Zapped never follows a redirect and does not accept a port other than 443, so put n8n behind a reverse proxy with a certificate and set the webhook base URL it should advertise.
- On a self hosted instance, the Code node is not allowed to load crypto until you allow it. Set the environment variable NODE_FUNCTION_ALLOW_BUILTIN to crypto and restart n8n.
- Decide where the lead should end up. This guide uses a contact in a CRM as the example.
Steps
- In n8n, add a Webhook node. Open its options and turn on Raw Body, so the workflow keeps the exact bytes that arrived.
- Copy the Production URL of the node. Do not use the Test URL: it only listens while you are testing, and it changes the address you have to save.
- Open Account, then Lead webhooks, paste the production address into Configure destination, tick the confirmation and choose Save destination.
- Copy the signing secret shown once. It starts with whsec_ and is never shown again.
- Add a Code node after the Webhook node, set it to Run Once for All Items, and paste this code. It reads the raw bytes from the binary data the Webhook node kept, checks the timestamp, and compares the signature.
const crypto = require('crypto');
const raw = (await this.helpers.getBinaryDataBuffer(0, 'data')).toString('utf8');
const headers = $input.first().json.headers;
const secret = 'PASTE_YOUR_SIGNING_SECRET_HERE';
const key = Buffer.from(secret.slice(6), 'base64');
const id = String(headers['webhook-id'] || '');
const time = String(headers['webhook-timestamp'] || '');
const signature = String(headers['webhook-signature'] || '');
const nowSeconds = Math.floor(Date.now() / 1000);
const fresh = Math.abs(nowSeconds-Number(time)) <= 300;
const expected = crypto.createHmac('sha256', key).update(id + '.' + time + '.' + raw).digest();
let verified = false;
if (key.length === 32 && id && time && raw && fresh) {
verified = signature.split(' ').some(function (part) {
if (part.indexOf('v1,') !== 0) return false;
const given = Buffer.from(part.slice(3), 'base64');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
return [{ json: { verified: verified, event_id: id, payload: JSON.parse(raw) } }];
- Add an If node after the Code node. Continue on the true output only when verified is true, and connect nothing to the false output. Everything you build next is safe, because an unverified request never reaches it.
- Add your destination action on the true output, for example the HubSpot node with Create/Update a contact, and map the fields from the parsed payload.
- In the Webhook node, set the response to answer immediately, so n8n replies before your later nodes run and the reply is fast.
- Request one test from the Test history tab in Zapped and watch the workflow run. The test carries invented values and null identifiers, so route it to a test path or switch the workflow off again afterwards.
- Capture one lead through your own published card to confirm the whole path, then check the record your action created.
With an immediate response, n8n answers before your later nodes run, so a 2xx in Lead history means n8n accepted the request, not that the CRM record exists. Check the workflow's execution list for that. A request that your own filter or gate rejects still reaches Zapped as Delivered, because the tool answered before it decided. Lead history tells you the request arrived, not that it was trusted. Use the tool's own history to see whether your check passed.
Why the raw body lives in binary data
The Webhook node always gives you the parsed body, the headers, the query and the route parameters. Turning on Raw Body adds the original request bytes as binary data on the item, under the property named data.
A Code node reads those bytes with the getBinaryDataBuffer helper and turns them into text. Use that text for the signature. Do not hash the parsed body, because re-writing JSON changes the bytes and the signature will not match.
The Crypto node cannot do this check on its own: its HMAC action takes the secret as plain text, and a Zapped secret is base64 text that has to be decoded first.
The two URLs, and why the choice matters
The Test URL only listens for a short time while you click to listen in the editor. The Production URL answers whenever the workflow is active, which is the address Zapped should have.
Changing between them means saving a new destination in Zapped, and saving a destination always creates a new signing secret. Plan for that: switch to the production address before you paste the secret into the Code node, then paste it once.
Which fields to map
The Code node above returns the parsed payload, so later nodes can read it under payload. The lead is under payload.data.lead, the card that captured it is under payload.data.card, the consent is under payload.data.consent, and your own questions are under payload.data.custom_fields. A missing answer arrives as null.
For a single line of answers to your custom questions, use an expression such as {{ $json.payload.data.custom_fields.answers.map(a => a.question_label + ': ' + (a.value ?? a.selected_options.map(o => o.option_label).join(', '))).join(' | ') }}.
The developer guide at /api-documentation/lead-webhooks documents every header, every payload field, the retry table, the test event and ready made PHP and Node.js verification snippets.
Stopping duplicates
A retry or a replay keeps the same event id, so the same lead can enter the workflow twice. Add the Remove Duplicates node with the Remove Items Processed in Previous Executions operation, after the If node, and set the value to compare to the event id from the payload, which is also the webhook-id header.
Where your CRM node offers an upsert, use it with the email address as the matching field. That also protects you from a visitor who sends the form twice.
A visitor who sends the form twice creates a second lead with a new event id and duplicate_of_lead_id set. That is a real second enquiry, not a retry.
Different cards, different handling
payload.data.card.id tells you which card captured the lead, and card.name gives its name. Use a Switch node after the verification to send leads from one card to one owner, one pipeline or one channel.
A test event has a null card id, so a rule built on a card id will not match a test. Test the card rule with a real lead from that card.
Plans and availability
Lead webhooks are included with Professional. Open Account, then Lead webhooks, and save your HTTPS destination to get started. Delivery begins only after you configure a destination and the service is available. Custom-plan access follows your agreement.
Good ways to use this
- Verify in the first Code node and gate everything behind the If node, so nothing writes before the request is trusted.
- Use the production address from the start, so you never replace the destination and its secret by accident.
- On a self hosted instance, set NODE_FUNCTION_ALLOW_BUILTIN to crypto and check that the workflow can still run after an upgrade.
- Answer immediately in the Webhook node, so a slow CRM call never turns into a retry from Zapped.
Common questions
Do I need to allow crypto in the Code node?
On n8n Cloud, crypto is provided already. On a self hosted instance, the Code node cannot load built in modules until you set NODE_FUNCTION_ALLOW_BUILTIN to crypto and restart n8n. Without it the Code node fails at the require line.
Can I check the signature with the Crypto node instead?
Not for a Zapped secret. The Crypto node's HMAC action takes a secret as text, and the whsec_ value has to be base64 decoded before it is used as a key. Do the check in a Code node.
Why can I not use a port other than 443?
A Zapped destination must be HTTPS on port 443 with a public hostname. If n8n runs on its default port, put it behind a reverse proxy that serves HTTPS on 443 and set the webhook base URL so n8n advertises the address you actually saved.
How do I check that verification really works?
Request one test and confirm the workflow passes the If node. Then temporarily change one character of the secret in the Code node and run the test again: the If node should send it down the false output. Put the real secret back afterwards.
If something looks wrong
- If the Code node fails with a module error, crypto is not allowed. Set NODE_FUNCTION_ALLOW_BUILTIN to crypto on the instance that runs the task and restart n8n.
- If the Code node cannot read the binary data, Raw Body is off, or the property name changed in your node's options. Turn Raw Body on and keep the property name data.
- If verified is always false, the payload was hashed after parsing instead of as raw bytes. Read the bytes with getBinaryDataBuffer and hash that text.
- If the workflow never runs, it is not active, or the destination holds the Test URL. Activate the workflow, copy the Production URL into Zapped and paste the new signing secret into the Code node.
- If the address was refused when you saved it, the address is not HTTPS on port 443 with a public hostname. Check the proxy and the webhook base URL n8n advertises.
- If a lead arrives twice, the duplicate check is missing or placed after the node that writes. Put Remove Duplicates before your destination node.