Email Verifier & Validator - Syntax, MX, Disposable Check avatar

Email Verifier & Validator - Syntax, MX, Disposable Check

Pricing

from $0.25 / 1,000 email verifieds

Go to Apify Store
Email Verifier & Validator - Syntax, MX, Disposable Check

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

Sumitr Mardy

Maintained by Community

Actor stats

1

Bookmarked

155

Total users

34

Monthly active users

a day ago

Last modified

Security

Limited permissions

Share

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:

  1. status is always one of four values. deliverable, undeliverable, risky, unknown. Never null, never empty, never a fifth value.
  2. Every field in checks is always present. All 11 booleans, every time. A check that was never reached is false — the reason lives in sub_status. No defensive null-checking in your code.
  3. 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 checks keys on every record, whether the SMTP layer ran or not. Nothing to branch on, nothing to guard.
  • status has exactly four values, forever. A caller can enumerate them once and be correct permanently. New information arrives in sub_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 unknown or risky with 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

FieldTypeDefaultDescription
emailsarray of strings[]Addresses to verify.
emailFileUrlstring—URL of a .txt (one per line) or .csv. Every email-shaped token in the file is picked up. Used in addition to emails.
concurrencyinteger 1–5015Domains checked in parallel. Addresses are grouped by domain first.
verbosebooleanfalseAdds 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

FieldGuarantee
emailYour input, echoed verbatim — even when it fails syntax.
normalized_emailLower-cased; +tag stripped; dots folded for Gmail. Empty string if unparseable, never null.
domainLower-cased domain, or empty string. Never null.
statusAlways one of 4 values.
sub_statusAlways one of the 12 codes below.
scoreAlways an integer 0–100.
checksAlways all 11 boolean keys. Never null inside.
mx_recordHighest-priority MX host, or empty string. Never null.
errorPresent only on unknown/internal_error.
rawPresent 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.

statussub_statusMeaningScore
deliverablevalid_mailbox †SMTP confirmed the mailbox exists and the domain is not catch-all.100 (90 on A-record fallback)
undeliverableinvalid_syntaxFails RFC 5322.0
undeliverableno_mx_recordNo MX and no A record (or RFC 7505 null MX).0
undeliverablemailbox_not_found †SMTP 550 on the real address, on a non-catch-all domain.5
riskycatch_all †Domain accepts every address; no individual mailbox can be confirmed.50
riskydisposableDomain is on the disposable/temp-mail blocklist.20
riskyrole_based †Shared inbox (info@, admin@, support@, …) that SMTP confirmed exists.60
riskyfull_mailbox †SMTP 452/552 — mailbox over quota.45
riskyfree_provider_unverifiableGmail/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
unknowngreylisted †421/450/451 — retry later. Not undeliverable.40
unknownsmtp_timeoutThe 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
unknowninternal_errorUnexpected 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

  • disposable is terminal — no SMTP is spent on a mailbox designed to expire.
  • catch_all beats role_based: on a catch-all domain nothing about the individual mailbox is knowable.
  • role_based beats deliverable: a confirmed info@ is a real mailbox but not a person.
  • A 5xx whose text mentions blocklists, reputation, or policy is treated as unknown/smtp_timeout, not mailbox_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 is unknown/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_found or catch_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@) return unknown with checks.role_based: true, so you can still filter them.
  • DNS failures are not verdicts. A resolver that fails (SERVFAIL, timeout) gives unknown, never a false no_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 on score alone: undeliverable never send, risky needs a policy (disposable, free_provider_unverifiable), and unknown means this actor could not judge the mailbox — it is not a bad address, and it was billed at the low email_unknown rate.
  • risky/free_provider_unverifiable on 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-actor tool waits up to 45 seconds. A long list keeps verifying after that: fetch the results with get-dataset-items and the run's datasetId.

Development

npm install
npm run build
npm 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