How to validate email, phone and IP in n8n
n8n has no native Boundstone node yet, so you wire validation through the generic HTTP Request node — here's the exact node config, credential setup, and IF branch.
Contents
You want to validate email in n8n — catch the typos and disposable addresses before they reach your CRM — and you went looking for the Boundstone node. There isn't one yet. A native node is planned, but today you wire Boundstone into n8n the same way you'd wire any REST API that doesn't ship its own node: the built-in HTTP Request node, one credential, and an IF node to act on the answer. It takes about five minutes, and the same pattern validates phone numbers and IP addresses too — you swap the URL and the field name.
What n8n can validate on its own
Before you add an API call, know what you don't need one for. n8n can check email syntax by itself. A Code node with a regex catches empty strings, missing @ signs, and fat-fingered domains without a single network request:
// n8n Code node (Run Once for All Items) — cheap syntax gate, no network
return $input.all().map(item => {
const email = item.json.email ?? "";
const looksValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
return { json: { ...item.json, looksValid } };
});
That regex is free, instant, and often all a form-cleanup step needs. What it can't tell you: whether the domain actually accepts mail, or whether @mailinator.com is a throwaway. For a one-off check you can paste an address into the free email validator — no signup. For validation inside a workflow, that's where the API earns its place: it runs the syntax check and the parts n8n can't.
Configure the HTTP Request node
Add an HTTP Request node after whatever feeds it addresses — a form trigger, a spreadsheet row, a webhook. Set it up:
- Method:
POST - URL:
https://api.boundstone.io/v1/verify/email - Send Body: on → Body Content Type:
JSON→ Specify Body: Using JSON
The JSON body pulls the email off the incoming item with an expression:
{ "email": "{{ $json.email }}" }
n8n sets Content-Type: application/json for you when the body type is JSON, so you don't add that header by hand.
Store the key in a credential, not the node
Do not type your key into the node. Set Authentication to Generic Credential Type, choose Header Auth, and create a credential with:
- Name:
Authorization - Value:
Bearer bs_live_YOUR_KEY
n8n encrypts credentials and keeps them out of workflow exports and execution logs. Your bs_live_ key is a server-side secret — n8n runs on the server, so it never reaches a browser, but a credential is still the right home for it. Inline in the node, it leaks the moment you export the workflow or share a screenshot.
Read the response and branch with an IF node
The HTTP Request node parses the response into $json for the next node:
{
"valid_syntax": true,
"domain": "example.com",
"mx_found": true,
"disposable": false,
"role_account": false,
"free_provider": false,
"checks": {
"performed": ["syntax", "mx", "disposable_list", "role_list"],
"not_performed": ["smtp_mailbox", "catch_all"]
}
}
Add an IF node after it. Use Boolean conditions and set the combinator to OR so any single red flag routes the item to the true output:
{{ $json.disposable }}is true{{ $json.mx_found }}is false{{ $json.valid_syntax }}is false
Wire the true output to your reject-or-hold path and the false output to the happy path. Disposable plus no MX record is a strong pair of signals for a signup gate; stacking more of them is the subject of multi-signal signup fraud.
The same node validates phone and IP
Duplicate the node, change the URL and body, and the rest of the pattern holds. The Header Auth credential works for all three endpoints.
For phone numbers, POST to https://api.boundstone.io/v1/verify/phone:
{ "phone": "{{ $json.phone }}" }
It returns valid, e164, country, line_type, and national_format. Branch on {{ $json.valid }} is false.
For IP addresses, POST to https://api.boundstone.io/v1/verify/ip:
{ "ip": "{{ $json.ip }}" }
It returns valid, version, normalized, classification, is_public, and is_bogon. Branch on {{ $json.is_bogon }} is true or {{ $json.is_public }} is false.
Validating a whole spreadsheet at once? POST a raw CSV body to /v1/bulk/email, /v1/bulk/phone, or /v1/bulk/ip and you get back HTTP 202 with a job_id; poll /v1/bulk/:id/results.csv for the finished file. The free tier caps a job at 250 rows, paid at 10,000, and refunds credits for any row that errors.
What "valid" actually means
Every response ends with a checks object, and the half that matters most is not_performed. Read it before you trust a green result.
- Email —
not_performedis["smtp_mailbox", "catch_all"]. Boundstone does not open an SMTP connection to the mailbox and does not detect catch-all domains. A "valid" here means the syntax parses, the domain has MX records, and it isn't on the disposable or role lists — not that a human reads that inbox. - Phone —
not_performedis["carrier_lookup", "ported_status", "hlr_liveness"]. No carrier lookup, no ported-status check, no HLR liveness ping by default. Those run as a paid opt-in withhlr:true, at 5 credits, refunded when the network cannot answer. - IP —
not_performedis["geolocation", "asn", "hosting_datacenter", "proxy_vpn_tor", "reputation"]. No geolocation, no ASN, no datacenter or VPN/proxy/Tor detection, no reputation score. That layer needs licensed data Boundstone doesn't ship today.
The not_performed list isn't fine print — it's the point. It tells you exactly what a "valid" did and didn't cover, so you never mistake "well-formed" for "reachable." The full field reference lives in the docs.
The short version
- No native Boundstone node yet — one is planned. Use the built-in HTTP Request node.
- POST to
https://api.boundstone.io/v1/verify/emailwith body{"email": "{{ $json.email }}"}. - Store the key in a Header Auth credential (
Authorization/Bearer bs_live_...), never inline in the node. - Add an IF node and branch on
disposable,mx_found, orvalid_syntax. - Swap the URL for
/v1/verify/phoneor/v1/verify/ip; the same credential covers all three. - Read
checks.not_performedon every response so you know what a "valid" didn't check.