Email Verifier & Validator - Syntax, MX, Disposable Check
Pricing
from $0.25 / 1,000 email verifieds
Email Verifier & Validator - Syntax, MX, Disposable Check
Bulk email verifier and validator, $0.40 per 1,000: RFC syntax, DNS/MX, disposable, role and free-provider detection. No mailbox (SMTP) check: addresses that would need one return unknown at $0.02 per 1,000. Flat JSON, never null. Works with n8n, Make, Zapier and AI agents via MCP.
Pricing
from $0.25 / 1,000 email verifieds
Rating
0.0
(0)
Developer
Sumitr Mardy
Maintained by CommunityActor stats
1
Bookmarked
155
Total users
34
Monthly active users
a day ago
Last modified
Categories
Security
Limited permissionsShare
Email Verification API — Bulk Syntax, MX, Disposable & Role Check
For a local-business lead pipeline, collect websites with Google Maps Scraper, find public contact emails with Website Contact Finder, then pass the email strings as emails here. Each Actor has separate pricing.
Verify email addresses in bulk: RFC 5322 syntax → DNS/MX → disposable, role and free-provider lists. One flat JSON record per address, built to be consumed by a machine.
Mailbox (SMTP) verification is not included. This actor does not connect to mail servers, so it cannot confirm that a specific mailbox exists. It catches the addresses that are certainly bad or risky — malformed, no mail server, disposable, free provider — and an address that would need a mailbox check returns
unknown, billed at a small fraction of the verdict price. You pay full price only for verdicts it can actually give.
Why this one
Most verifiers are easy to use until you put them in a pipeline. Then you find out that status is sometimes null, that checks.catch_all disappears when the check didn't run, and that "valid" quietly means "the domain accepted a random string".
This actor makes three promises and enforces them in tests:
statusis always one of four values.deliverable,undeliverable,risky,unknown. Never null, never empty, never a fifth value.- Every field in
checksis always present. All 11 booleans, every time. A check that was never reached isfalse— the reason lives insub_status. No defensive null-checking in your code. - What cannot be known is reported as
unknown, never guessed. No address is ever called "valid" without proof. See Known limits.
Built to be called by a machine
Most verifiers are written for a human reading a dashboard. This one assumes the caller is code — a pipeline step, a workflow node, or an AI agent holding it as a tool.
That distinction is not marketing. It changes what the output has to guarantee.
A human sees catch_all missing from a response and shrugs. An agent reasoning over the same response concludes the domain is not catch-all, marks the address safe, and sends. An agent cannot write result.checks?.catch_all ?? false — it acts on what it reads. So a field that sometimes disappears is not a minor inconvenience; it is a wrong answer with no warning attached.
What that means here:
- The response shape never varies. Same 11
checkskeys on every record, whether the SMTP layer ran or not. Nothing to branch on, nothing to guard. statushas exactly four values, forever. A caller can enumerate them once and be correct permanently. New information arrives insub_status, which is also always populated.- Verdicts are deterministic. Scores come from a lookup table keyed on the verdict — no model, no randomness. The same address with the same verdict scores the same today and in a year, which is what makes a result cacheable and a test repeatable.
- Uncertainty is a value, not a gap. When the mailbox genuinely cannot be determined, you get
unknownorriskywith a reason — never a confident guess and never a null. An agent that can distinguish "this is dead" from "I could not find out" makes better decisions than one handed a boolean.
Being a standard Apify Actor, it is callable from anything that speaks the Apify API — including agent toolchains and MCP integrations — and the input schema doubles as the tool's parameter description.
Input
| Field | Type | Default | Description |
|---|---|---|---|
emails | array of strings | [] | Addresses to verify. |
emailFileUrl | string | — | URL of a .txt (one per line) or .csv. Every email-shaped token in the file is picked up. Used in addition to emails. |
concurrency | integer 1–50 | 15 | Domains checked in parallel. Addresses are grouped by domain first. |
verbose | boolean | false | Adds a raw block per record with per-layer detail (MX list, A records, cache hits). Off by default to keep records small. |
At least one of emails / emailFileUrl must yield an address, otherwise the run ends immediately with a clear status message and charges nothing. |
{"emails": ["john@example.com","info@apify.com","someone@gmail.com","test@mailinator.com"],"concurrency": 15,"verbose": false}
Output
One record per input address, pushed to the dataset:
{"email": "john@example.com","normalized_email": "john@example.com","domain": "example.com","status": "deliverable","sub_status": "valid_mailbox","score": 100,"checks": {"syntax": true,"domain_exists": true,"mx_found": true,"a_record_fallback": false,"smtp_connectable": true,"mailbox_exists": true,"catch_all": false,"disposable": false,"role_based": false,"free_provider": false,"has_tag": false},"mx_record": "aspmx.l.google.com","verified_at": "2026-08-14T10:00:00.000Z","duration_ms": 340}
Field guarantees
| Field | Guarantee |
|---|---|
email | Your input, echoed verbatim — even when it fails syntax. |
normalized_email | Lower-cased; +tag stripped; dots folded for Gmail. Empty string if unparseable, never null. |
domain | Lower-cased domain, or empty string. Never null. |
status | Always one of 4 values. |
sub_status | Always one of the 12 codes below. |
score | Always an integer 0–100. |
checks | Always all 11 boolean keys. Never null inside. |
mx_record | Highest-priority MX host, or empty string. Never null. |
error | Present only on unknown/internal_error. |
raw | Present only when verbose: true. |
An unexpected failure on one address produces unknown / internal_error with an error string. It never aborts the run or drops a record.
Status × sub_status — the full table
Rows marked † need a mailbox (SMTP) check, which this actor does not perform, so they are not returned. They stay in the table because they are part of the output contract.
status | sub_status | Meaning | Score |
|---|---|---|---|
deliverable | valid_mailbox † | SMTP confirmed the mailbox exists and the domain is not catch-all. | 100 (90 on A-record fallback) |
undeliverable | invalid_syntax | Fails RFC 5322. | 0 |
undeliverable | no_mx_record | No MX and no A record (or RFC 7505 null MX). | 0 |
undeliverable | mailbox_not_found † | SMTP 550 on the real address, on a non-catch-all domain. | 5 |
risky | catch_all † | Domain accepts every address; no individual mailbox can be confirmed. | 50 |
risky | disposable | Domain is on the disposable/temp-mail blocklist. | 20 |
risky | role_based † | Shared inbox (info@, admin@, support@, …) that SMTP confirmed exists. | 60 |
risky | full_mailbox † | SMTP 452/552 — mailbox over quota. | 45 |
risky | free_provider_unverifiable | Gmail/Outlook/Yahoo/iCloud etc. Their RCPT answers carry no information, so no mailbox verdict is claimed. Decided by the domain, not by whether the SMTP session succeeded. | 55 |
unknown | greylisted † | 421/450/451 — retry later. Not undeliverable. | 40 |
unknown | smtp_timeout | The address needs a mailbox check, which is not performed — billed as email_unknown, a small fraction of the verdict price. Also: host unreachable, timed out, rejected the session on policy/IP-reputation grounds, or the DNS resolver failed (SERVFAIL) instead of answering. | 30 |
unknown | internal_error | Unexpected processing error; error field explains. | 0 |
Scores are a pure function of the verdict — no randomness, no model. The same address with the same verdict scores the same forever.
Precedence, so nothing surprises you
disposableis terminal — no SMTP is spent on a mailbox designed to expire.catch_allbeatsrole_based: on a catch-all domain nothing about the individual mailbox is knowable.role_basedbeatsdeliverable: a confirmedinfo@is a real mailbox but not a person.- A
5xxwhose text mentions blocklists, reputation, or policy is treated asunknown/smtp_timeout, notmailbox_not_found. Rejections aimed at the sender are never turned into claims about the recipient. - The same rule applies one layer down: an authoritative NXDOMAIN is
undeliverable/no_mx_record, but a resolver SERVFAIL or timeout isunknown/smtp_timeout. "We could not find out" never becomes "it does not exist".
How verification works
Layer 1 — Syntax. RFC 5322 via validator.js. Invalid → terminal.
Layer 2 — DNS. MX lookup, sorted by preference. No MX but an A record → RFC 5321 implicit MX, flagged as a_record_fallback. Neither → no_mx_record, unless the resolver failed rather than answered, which is unknown.
Layer 3 — Lists. Disposable blocklist (refreshed daily from the public disposable-email-domains list, ~8k domains, with a bundled fallback), role-account local parts, free-provider domains, +tag detection.
Layer 4 — SMTP: not performed. Confirming a mailbox means connecting to the recipient's mail server on port 25, which Apify blocks for every actor. Rather than guess, the actor stops after Layer 3: addresses on business domains, where only a mailbox check could decide, come back unknown/smtp_timeout with checks.smtp_connectable: false, billed at the low email_unknown rate.
Known limits
Stated openly, because you would find them anyway:
- No mailbox check. The actor never confirms that a mailbox exists, so it never returns
deliverable,mailbox_not_foundorcatch_all. Use it to remove the addresses that are certainly bad before sending; the ones it cannot judge cost a small fraction of a verdict. - Free providers. Gmail, Outlook, Yahoo, iCloud and the like return
risky/free_provider_unverifiable: the domain is real and accepts mail, nothing more is claimed. - Role accounts on business domains (
info@,sales@) returnunknownwithchecks.role_based: true, so you can still filter them. - DNS failures are not verdicts. A resolver that fails (SERVFAIL, timeout) gives
unknown, never a falseno_mx_record.
Performance & cost
- MX cache, keyed by domain, 24h TTL, persisted in the key-value store — so it survives across runs. A 5,000-address list is usually a few hundred domains.
- Domain grouping. Addresses are grouped by domain, so each domain is resolved once.
- Results stream to the dataset in batches while the run is still going.
Every run logs its own KPIs: throughput, the full status/sub_status distribution, cache hit rates, and the unknown rate.
Pricing
Pay per event — email_verified. Current rates are on the Pricing tab.
Two events. email_verified for an address with a definite verdict: invalid_syntax, no_mx_record, disposable, free_provider_unverifiable. email_unknown for an address that needed a mailbox check, which this actor does not perform — priced at a small fraction of a verdict, enough to cover the work of checking it. No per-run charge.
Usage
Via the Apify API (Node):
$npm install apify-client
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_TOKEN' });const run = await client.actor('sumitr_mardy/email-verifier').call({emails: ['john@example.com', 'info@apify.com'],concurrency: 15,});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const r of items) {console.log(r.email, r.status, r.sub_status, r.score);}
Filtering, without a single null check:
const mailable = items.filter((r) => r.status === 'deliverable');const worthRetrying = items.filter((r) => r.status === 'unknown');const neverSend = items.filter((r) => r.status === 'undeliverable' || r.checks.disposable);
Use it from AI agents (MCP)
This Actor is a tool on the Apify MCP server, so Claude, Cursor, VS Code or any MCP client can call it. Load only this tool: a short tool list keeps the agent from picking the wrong one.
Recommended: hosted server, OAuth sign-in (nothing secret on disk). Add to your MCP client config (claude_desktop_config.json, .cursor/mcp.json, …):
{"mcpServers": {"email-verifier": {"url": "https://mcp.apify.com?tools=sumitr_mardy/email-verifier"}}}
With an API token instead (clients without OAuth, or unattended setups):
{"mcpServers": {"email-verifier": {"url": "https://mcp.apify.com?tools=sumitr_mardy/email-verifier","headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }}}}
Local server (stdio):
{"mcpServers": {"email-verifier": {"command": "npx","args": ["-y", "@apify/actors-mcp-server@latest", "--tools", "sumitr_mardy/email-verifier"],"env": { "APIFY_TOKEN": "YOUR_APIFY_TOKEN" }}}}
Get a token at Apify Console → API & Integrations, and never commit it. Runs are billed per address to the Apify account the agent signs in with, at the normal price.
Tips for agents:
- Act on
status, never onscorealone:undeliverablenever send,riskyneeds a policy (disposable,free_provider_unverifiable), andunknownmeans this actor could not judge the mailbox — it is not a bad address, and it was billed at the lowemail_unknownrate. risky/free_provider_unverifiableon Gmail, Outlook or Yahoo is the honest answer, not a failure: those providers do not reveal whether a mailbox exists.- Pass the whole list in one call rather than one call per address. Each domain is resolved once and reused for every address on it.
- The MCP
call-actortool waits up to 45 seconds. A long list keeps verifying after that: fetch the results withget-dataset-itemsand the run'sdatasetId.
Development
npm installnpm run buildnpm test
npm test runs the contract tests — no network required. They assert the field guarantees above, the SMTP code mapping, and score determinism.
The accuracy gate makes real DNS and SMTP calls against a labelled set:
$npm run accuracy -- test/fixtures/labeled.csv
It prints per-group accuracy and the unknown rate, and exits non-zero below the 95% target. The own-domain-valid, own-domain-invalid and catch-all groups need a throwaway side-project domain you control — those are the branches that carry the product. Do not bulk-probe your main company domain.
Latest measured run: 100% sub_status match over 166 labelled rows, 1.8% unknown — scored identically running direct and running through the relay, which is how the transport was shown to be equivalent.
To score a run that happened elsewhere — an Apify run, whose IP is the one that actually matters — export its dataset as JSON and pass it in. Nothing is verified locally; the exported records are scored against the same labels:
$npm run accuracy -- --dataset path/to/dataset.json