# How to validate email, phone and IP in n8n

_2026-08-17 · Boundstone (https://boundstone.io/blog/validate-data-n8n)_


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:

```js
// 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](/tools/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:

```json
{ "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:

```json
{
  "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](/blog/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`:

```json
{ "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`:

```json
{ "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_performed` is `["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_performed` is `["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 with `hlr:true`, at 5 credits, refunded when the network cannot answer.
- **IP** — `not_performed` is `["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](/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/email` with 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`, or `valid_syntax`.
- Swap the URL for `/v1/verify/phone` or `/v1/verify/ip`; the same credential covers all three.
- Read `checks.not_performed` on every response so you know what a "valid" didn't check.
