Automatically Verify New Leads With Zapier, Make or a Webhook
To verify leads automatically, trigger on the new lead, call an email verification API with a short timeout, map the answer to valid, invalid or unconfirmed, then tag or route the lead in your CRM. Zapier and Make can do this without servers, within their plan limits; a small webhook receiver does it with no per-task cost.
The design matters more than the tool. Broken setups usually fail in one of three ways: the step times out before the verifier answers, a timeout is treated as "valid", or the form waits on verification and the lead is lost when it is slow.
The flow, in five steps
Every version of this automation has the same shape, whichever platform runs it.
- Trigger on a new lead: a form submission, a new CRM contact, a row in a sheet.
- Pre-check cheaply: trim, lower-case, reject anything without an
@. No API call needed. - Verify with an API call that has an explicit timeout.
- Map the vendor's answer to three outcomes:
valid,invalid,risky. - Route: valid leads go into sequences, invalid ones are tagged and kept out, risky ones are tagged unconfirmed and held back.
Step 4 is where the real decisions happen. A catch-all result, a timeout and an "unknown" are not the same as valid. All three mean nobody confirmed the mailbox, so they belong in the middle bucket. We cover vendor vocabularies in detail in our verification API comparison.
Plan limits that decide the design
These come from Zapier's and Make's own help pages, read on 1 October 2026. They change, so check them before you build.
| Limit | Zapier | Make |
|---|---|---|
| Inbound webhooks | Webhooks by Zapier on Professional, Team, Enterprise; not Free (source) | Free-plan availability not stated on the pricing page |
| What the caller gets back | 200 while the Zap is on, 404 when it is off | "Accepted" with 200, unless you add a Webhook response module (source) |
| Response timeout | Not applicable; no custom response from Catch Hook | 180 seconds, then 200 Accepted (source) |
| Max payload | 10 MB for triggers, 2 MB for Catch Raw Hook | 5 MB, any tier |
| Inbound rate | Instant triggers over 20,000 requests per 5 minutes per user get 429 (source) | 300 webhook requests per 10 seconds, then 429 |
| Code step runtime | Free 1 s; Professional and Team 30 s; Enterprise 2 min (source) | Not applicable (use the HTTP module) |
| Free plan | 100 tasks a month, two-step Zaps (source) | 1,000 credits a month (source) |
Two rows matter more than the rest.
The 1-second Code step on Zapier Free. Verification vendors' own default timeouts are far longer: ZeroBounce defaults to 30 seconds, Kickbox to 6,000 ms, Bouncer to 10 seconds. An SMTP check against a slow mail server can easily outlast one second, so on the Free plan a Code step calling a verifier will often fail. The Free plan's two-step limit also leaves no room for a separate routing step. In practice, lead verification in Zapier is a paid-plan workflow.
Make's 180-second response window. It is tempting to have the form wait for Make to return the verification result. Do not. If the scenario is slow, the caller gets a bare "Accepted" and your form code has to cope anyway.
Option 1: Zapier with a Code step
On a paid plan the simplest Zap is: trigger (your form app, or Catch Hook) → Code by Zapier (JavaScript) → Paths or Filter → CRM action.
Zapier's JavaScript steps receive your mapped fields in inputData, return results through a single output variable, and include fetch. This step uses ZeroBounce's documented validate endpoint, with timeout in seconds, and sets it to 20 so it fits inside a 30-second Professional step with margin.
// Code by Zapier (JavaScript). Input Data: email, apiKey.
const email = (inputData.email || "").trim().toLowerCase();
if (!email.includes("@")) {
output = { outcome: "invalid", raw: "no_at_sign", email };
} else {
const url = new URL("https://api.zerobounce.net/v2/validate");
url.searchParams.set("api_key", inputData.apiKey);
url.searchParams.set("email", email);
url.searchParams.set("timeout", "20"); // seconds; stays inside a 30 s step
try {
const res = await fetch(url, { signal: AbortSignal.timeout(25000) });
const body = await res.json();
const raw = String(body.status || `http_${res.status}`);
const outcome = raw === "valid" ? "valid"
: ["invalid", "spamtrap", "abuse", "do_not_mail"].includes(raw) ? "invalid"
: "risky";
output = { outcome, raw, sub_status: body.sub_status || "", email };
} catch (err) {
output = { outcome: "risky", raw: err.name, sub_status: "", email };
}
}
We tested it in Node.js 22 (the runtime Zapier documents for Code steps) by running the code inside an async function with inputData, the way Zapier does, against a local mock of the ZeroBounce response:
{"outcome":"valid","raw":"valid","sub_status":"","email":"jane@example.com"}
{"outcome":"invalid","raw":"invalid","sub_status":"mailbox_not_found","email":"bad@example.com"}
{"outcome":"risky","raw":"catch-all","sub_status":"","email":"sam@catchall.test"}
{"outcome":"invalid","raw":"no_at_sign","email":"nonsense"}
{"outcome":"risky","raw":"TimeoutError","sub_status":"","email":"slow@example.com"} (after 25 s)
The last line is the important one. A hung mail server becomes risky, never valid, and the step finishes inside its time limit.
Then add Paths: one path where outcome is valid (add to the sequence), one where it is invalid (tag, do not sequence), and a fallback for everything else (tag unconfirmed). Store raw in a CRM field as well; it is the evidence when someone asks why a lead was held back.
One caution: values mapped into inputData show in Zap history. Treat the API key accordingly, and rotate it if the Zap is shared.
Some verification vendors also publish their own Zapier integrations; Kickbox, for example, documents Zapier recipes for CRMs such as ActiveCampaign and Act-On. Those avoid the Code step entirely, at the cost of the vendor's status vocabulary reaching your Paths unmapped.
Option 2: Make with the HTTP module
Make's equivalent is: Custom webhook (or your form app's module) → HTTP "Make a request" to the verifier → Router → CRM modules.
Three settings to get right:
- Put the timeout in the URL. Add the vendor's timeout parameter to the query string, in that vendor's unit. Kickbox and Clearout take milliseconds; ZeroBounce and Bouncer take seconds.
- Do not parse an error as a result. Add an error handler on the HTTP module that routes to the "unconfirmed" branch. A
429or a5xxis not a verdict on the address. - Acknowledge first. Without a response module, Make replies "Accepted" by default. If the caller needs a custom reply, put the Webhook response module straight after the trigger, then verify.
Router filters then mirror the Zapier Paths: status equals the vendor's "good" value, equals its "bad" values, and a fallback route with no filter for everything else.
Option 3: your own webhook receiver
If you have a backend, a small receiver avoids per-task pricing and gives you control over retries. The pattern is the standard one for webhooks: verify the signature, acknowledge quickly, process afterwards, and ignore duplicates.
// receiver.mjs: accept a lead webhook, acknowledge fast, verify and route afterwards.
import http from "node:http";
import crypto from "node:crypto";
const SECRET = process.env.WEBHOOK_SECRET;
const VERIFY_URL = process.env.VERIFY_URL; // vendor endpoint; add its auth as its docs require
const CRM_URL = process.env.CRM_URL; // where verified leads go
const seen = new Map(); // lead id -> time; use Redis or a DB table in production
function signatureOk(rawBody, header) {
const expected = crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
const given = Buffer.from(String(header || ""), "utf8");
const want = Buffer.from(expected, "utf8");
return given.length === want.length && crypto.timingSafeEqual(given, want);
}
async function verify(email) {
try {
const url = new URL(VERIFY_URL);
url.searchParams.set("email", email);
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
const body = await res.json();
const raw = String(body.status ?? `http_${res.status}`);
if (raw === "valid") return { outcome: "valid", raw };
if (raw === "invalid") return { outcome: "invalid", raw };
return { outcome: "risky", raw };
} catch (err) {
return { outcome: "risky", raw: err.name };
}
}
async function processLead(lead) {
const result = await verify(lead.email);
const tag = { valid: "email-verified", invalid: "email-invalid", risky: "email-unconfirmed" }[result.outcome];
await fetch(CRM_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...lead, verification: result.raw, tag, sequence: result.outcome === "valid" }),
});
console.log(`${lead.id} ${lead.email} -> ${result.outcome} (${result.raw})`);
}
http.createServer((req, res) => {
if (req.method !== "POST") { res.writeHead(405).end(); return; }
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const raw = Buffer.concat(chunks);
if (!signatureOk(raw, req.headers["x-signature"])) { res.writeHead(401).end(); return; }
let lead;
try { lead = JSON.parse(raw); } catch { res.writeHead(400).end(); return; }
if (!lead.id || !lead.email) { res.writeHead(422).end(); return; }
if (seen.has(lead.id)) { res.writeHead(200).end("duplicate"); return; }
seen.set(lead.id, Date.now());
res.writeHead(202).end("accepted"); // answer the sender before doing slow work
processLead(lead).catch((err) => console.error(lead.id, err.message));
});
}).listen(process.env.PORT || 3000);
To test it, sign a lead the way your form backend would:
// send.mjs: node send.mjs <lead-id> <email> [secret]
import crypto from "node:crypto";
const [id, email, secret = process.env.WEBHOOK_SECRET] = process.argv.slice(2);
const body = JSON.stringify({ id, email, name: "Test Lead" });
const sig = crypto.createHmac("sha256", secret).update(body).digest("hex");
const t = Date.now();
const res = await fetch("http://127.0.0.1:3000/", { method: "POST", headers: { "x-signature": sig, "content-type": "application/json" }, body });
console.log(id, res.status, await res.text(), `${Date.now() - t}ms`);
We ran it on Node.js 22 against a mock verifier that deliberately took 1.5 seconds per address and a mock CRM:
L1 202 accepted 21ms
L2 202 accepted 19ms
L3 202 accepted 19ms
L1 200 duplicate 19ms
L4 401 18ms
CRM got {"id":"L1","email":"jane@example.com",...,"verification":"valid","tag":"email-verified","sequence":true}
CRM got {"id":"L2","email":"bad@example.com",...,"verification":"invalid","tag":"email-invalid","sequence":false}
CRM got {"id":"L3","email":"sam@catchall.test",...,"verification":"catch-all","tag":"email-unconfirmed","sequence":false}
The sender got its answer in about 20 ms although verification took 1.5 seconds. The repeated L1 was acknowledged but not verified twice, and the lead signed with the wrong secret was refused.
Before production, replace the in-memory seen map with a database table, since it empties on restart, and put processLead behind a queue so a crash mid-verification can be retried. The response mapping in verify() assumes a vendor whose field is status with values valid and invalid; adjust it for yours.
Which option to choose
| Situation | Choose | Reason |
|---|---|---|
| Low volume, no developer, already on a paid Zapier plan | Zapier Code step | Fewest moving parts |
| Branching logic, several CRMs, cost-sensitive | Make | Router and error handlers are first-class |
| Own backend, steady volume | Webhook receiver | No per-task cost; full control of retries |
| Zapier Free plan | Not recommended | 1 s Code runtime, two-step limit, no webhooks |
What to do with each outcome
Verification only pays off if the routing is strict:
- Valid: into the sequence. Re-verify later if the lead sits unused for months; see how often to re-verify.
- Invalid: keep the record for reporting, never sequence it. If the vendor returned a typo suggestion, a human may be able to rescue the lead.
- Risky: tag it unconfirmed. Send manually or in a small separate batch, or wait for a reply or click before it enters automation.
If you also collect signups from the public, verification is not a replacement for confirmation; double opt-in and verification solve different problems.
The takeaway
Acknowledge the lead instantly, verify with an explicit timeout that fits inside your platform's step limit, and route anything unconfirmed to the middle bucket rather than into a sequence. Before automating, run a handful of real leads through the free email verification tool to see which outcomes your sources actually produce.
Common questions
Is Webhooks by Zapier free?
No. Zapier's help centre lists Webhooks by Zapier as available on Professional, Team and Enterprise plans, not Free. You can still verify leads on the Free plan if the trigger is a native app, such as a form tool, but the Free plan is limited to two-step Zaps.
Can a Code by Zapier step call an email verification API?
Yes, fetch is available in JavaScript Code steps. The catch is runtime: Zapier documents a 1-second limit on Free and 30 seconds on Professional and Team. A single SMTP verification can take several seconds, so set the vendor's timeout parameter well inside your step's limit.
What should the automation do with catch-all or unknown results?
Keep the lead, tag it as unconfirmed, and hold it out of automated sequences until something else confirms it, such as a reply or a click. Deleting it throws away real people; sequencing it unchecked is how bounce rates climb.
Should the webhook return the verification result to the form?
Usually not. Acknowledge the webhook immediately and verify afterwards. Zapier's Catch Hook returns 200 on its own, and Make's Webhook response module gives up after 180 seconds. If you need to warn the user about a typo while they are still on the page, do that in the form itself.
Verify unlimited addresses for $29.99/month
Real SMTP mailbox checks. No credits, no per-email fees.
Get Started