Home / Integrations / n8n
Validate Emails in n8n Workflows With ContactLint
Published 2026-10-18
This guide adds a ContactLint email check to an n8n workflow with the HTTP Request node. A small Code node turns each result into one action: reject, suggest or accept. A Switch node routes on that action, and the workflow fails open if ContactLint does not answer.
Where the check goes in n8n
Put the check right after your trigger and before any node that saves the contact, adds it to a CRM, or sends a welcome email. The trigger is usually a Webhook node that receives form posts, or a trigger from your form or CRM app.
n8n runs on a server, so your secret key stays out of the browser. That makes this a real server-side check. If your form waits for the webhook's response, you can return a "Did you mean ...?" message to the person. If it does not wait, use the result to tag or hold the record instead.
Setup: credential and HTTP Request node
- Get a free secret key at /signup.
- In n8n, create a new credential of type Header Auth. Set Name to
Authorizationand Value toBearer YOUR_SECRET_KEY. This keeps the key out of the workflow itself. - Add an HTTP Request node after the trigger and set:
- Method:
POST - URL:
https://contactlint.com/v1/email/validate - Authentication: Generic Credential Type, then Header Auth, then your credential
- Send Body: on, Body Content Type: JSON, Specify Body: Using Fields Below
- Body field name
email, value{{ $json.body.email }}for a Webhook trigger, or the field from your trigger - Options, Timeout:
3000(milliseconds)
- Method:
- Open the node's Settings and set On Error to continue. A timeout, 401 or 402 then passes an item without a
resultinstead of stopping the workflow. That is what makes it fail open. - Add a Code node after it, mode Run Once for All Items, language JavaScript:
return $input.all().map(item => {
const r = item.json.result;
let action = 'accept';
let message = '';
if (r && r.verdict === 'invalid') {
action = 'reject';
message = 'That email address cannot receive mail.';
} else if (r && r.did_you_mean) {
action = 'suggest';
message = 'Did you mean ' + r.did_you_mean + '?';
} else if (r && r.disposable) {
action = 'reject';
message = 'Please use a permanent email address.';
}
// No result means a timeout or API error: action stays 'accept' (fail open)
return { json: { ...item.json, action, message, suggestion: (r && r.did_you_mean) || '' } };
});- Add a Switch node in Rules mode with three outputs:
{{ $json.action }}equalsreject,suggestandaccept.
The HTTP Request node replaces the item with the API response. In later nodes, read the original address with an expression such as {{ $('Webhook').item.json.body.email }}.
Handling each verdict and did_you_mean
- invalid (action reject): the domain has no MX or A record, or the address is malformed. If your Webhook node responds using a Respond to Webhook node, return an error status with
{{ $json.message }}. For leads, mark the record instead of deleting it. - risky with reason
possible_typo(action suggest):did_you_meanholds the fix. Return "Did you mean ...?" so the form can ask the person to confirm. Or save the suggestion in a CRM field for review. The Code node checksdid_you_meanbeforedisposablebecause gmial.com is both a typo and on the disposable list. - risky with reason
disposable(action reject): drop or flag it if you need a permanent address. Change the Code node to accept it if you do not. - risky with reason
role_address(action accept): info@ and sales@ are real, shared inboxes. - valid or unknown (action accept): continue. An unknown caused by a failed DNS lookup is not billed, so you can check it again later.
Testing it
Click to listen for a test event on the Webhook node, then send test posts to its test URL:
curl -X POST https://your-n8n-host/webhook-test/signup \
-H "content-type: application/json" \
-d '{"email":"jane@gmial.com"}'jane@gmial.com: verdictrisky, reasonpossible_typo,did_you_meanjane@gmail.com, action suggest.disposableis also true.x@mailinator.com: verdictrisky, reasondisposable, action reject.jane@example.invalid: verdictinvalid, action reject.- Fail open: edit the credential to a wrong key. The API returns 401, the item has no result, and the action should be accept. Put the real key back.
Limits
- ContactLint does not connect to mail servers. It cannot confirm that a mailbox exists or detect catch-all domains.
- Fail open is deliberate. A slow or failed check never blocks a signup.
- A key is limited to 60 requests per minute. A 402 means the 100 free monthly checks are used and no credits remain. With On Error set to continue, the workflow keeps accepting until you buy credits or the month resets.
- For existing lists, use
POST /v1/email/validate/batch, which takes up to 50 addresses per request. See cleaning a CSV list. - To show hints in the browser before submit, add the widget and keep this workflow as the backstop.
Questions
Why use a Code node instead of putting all the rules in the Switch node?
The order matters: invalid first, then did_you_mean, then disposable. The Code node applies that order in one place and turns errors into accept. The Switch node then only compares one simple action field.
Where should I store the ContactLint key in n8n?
Store it in a Header Auth credential, not in the workflow. Workflows are often exported or shared, and credentials are kept separately.
Can I check many addresses at once?
Yes. Use POST /v1/email/validate/batch with an emails array of up to 50 addresses. Each address uses one check, and the batch counts as one request against the 60 per minute limit.
Other integrations
- Next.js
- Express
- Django
- FastAPI
- Laravel
- Ruby on Rails
- Supabase
- Firebase Authentication
- WordPress
- Webflow
- Zapier
- Make
Get a free API key. 100 checks per month, no card required. Or try a live check first.