Email Verifier - SMTP, Catch-All & Disposable Detection avatar

Email Verifier - SMTP, Catch-All & Disposable Detection

Pricing

$2.00 / 1,000 decisive verifications

Go to Apify Store
Email Verifier - SMTP, Catch-All & Disposable Detection

Email Verifier - SMTP, Catch-All & Disposable Detection

Bulk email verification that only bills decisive answers. Returns valid / invalid / disposable / catch-all / unknown with a confidence score; catch-all and unknown are always free, and duplicates are folded before billing.

Pricing

$2.00 / 1,000 decisive verifications

Rating

0.0

(0)

Developer

Chloe & Matt

Chloe & Matt

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

9 days ago

Last modified

Share

Email Verifier — decisive verdicts only, never billed for a maybe

Give it a list of email addresses. It removes the ones that provably cannot receive mail, flags the throwaways, folds the duplicates — and charges you nothing for the addresses it could not decide.

Three verdicts cost money: valid, invalid, disposable. Two are always free: catch_all and unknown.

That is the whole product. Everything below is how it earns the claim.


Read this before you run it

Apify Actors run on AWS EC2, which blocks outbound TCP on ports 25, 465 and 587. Mailbox-level SMTP verification is impossible from the platform's default network — not slow, not unreliable: impossible. Apify Proxy does not help either; CONNECT host:25 hangs while CONNECT host:443 completes in 45 ms. Measured on 2026-08-02 from a live Actor run.

So this Actor runs in one of two modes, and it tells you which one you are in:

Without a relay (default on Apify)With a port-25 relay
Syntax, RFC 5322yesyes
Domain / MX / null-MX / unreachable-MXyesyes
Disposable (11,582 domains)yesyes
Role, free provider, typo suggestion, de-dupeyesyes
Mailbox exists?no — every such address returns unknown, freeyesvalid / invalid at score 95

Set proxyUrl to an HTTP proxy that permits CONNECT host:25, or to a SOCKS5 proxy, and mailbox checks switch on. A purpose-built relay for this - password authentication, port-25-only allowlist, and a DATA filter so a leaked credential cannot send mail - ships alongside this Actor as smtp-probe-relay, deployable from a Dockerfile on any host whose provider permits port 25. The Actor probes the route once at startup; if port 25 is unreachable it says so in the log and skips straight to DNS-level verification instead of stalling on every domain in your list.

Every other verifier on this Store is silent about this. Several are resellers of an off-platform HTTP API, which is how they get around it. This one tells you what it did and does not bill you for what it could not do.

Audited accuracy — 2026-08-02

Run node benchmark/run.mjs to reproduce. It builds a 169-row benchmark whose ground truth is known by construction, runs it on the platform, and re-checks every billable verdict against an independent resolver (Cloudflare DNS-over-HTTPS), so the audit cannot simply agree with itself.

Composition: 60 live mail domains × 2 addresses, 15 non-existent domains, 15 malformed addresses, 10 disposable providers, 4 typo'd consumer domains, 5 duplicates. DNS-level mode, no relay.

measureresult
false invalid verdicts0 of 31 (0.00%)
false disposable verdicts0 of 12
non-existent domains caught15 of 15
malformed addresses caught15 of 15
typos suggested4 of 4
typo suggestions on healthy domains (false positives)0 of 120
duplicates folded before billing4 of 4
decisive (billed) share of this list26.06%
free of charge73.94%

Read the last two rows honestly: this benchmark is deliberately hostile — 71% of its rows sit on healthy corporate domains where only a mailbox check could decide anything, and it was run without a relay. On a scraped list, the decisive share is whatever fraction of it is genuinely broken. You pay for that fraction and nothing else.

The mailbox-check path is covered by protocol-level tests that run a real SMTP conversation (npm test, 42 assertions), including through HTTP CONNECT and SOCKS5 tunnels. It is not covered by the figures above, because the audit ran on Apify where port 25 is blocked.

Why the free verdicts matter

Every SMTP verifier hits three walls. What separates them is what they do next.

SituationWhat most verifiers returnWhat this Actor returns
Domain accepts every recipientvalid, or a silent unknowncatch_all, not billed
Our IP is blocked by the receiverinvalidunknown, not billed
Greylisted / rate limitedinvalid or unknownunknown, not billed
Port 25 unreachableinvalid, or a crashed rununknown, not billed
Mailbox genuinely rejectedinvalidinvalid, billed

The mechanism is a calibration probe. Before checking any of your addresses, the Actor asks the server about one random address on the same domain:

  • The server rejects it → the server really does check recipients, so its answers about your addresses are evidence. Results carry checks.calibrated: true and score 95.
  • The server accepts it → the domain is catch-all. Your addresses come back catch_all, cost nothing, and are not probed further.
  • The server blocks or defers us → nothing here is knowable right now. Everything comes back unknown and costs nothing.

The probe itself is never billed. Without it (detectCatchAll: false) an accept is worth only 72, because you cannot tell a real mailbox from a wide-open door.

The classifier is built around the same distinction. Microsoft 365 answers an unknown recipient with 550 5.4.1 Recipient address rejected: Access denied — that string contains "access denied", which naive verifiers read as a policy block. A reputation refusal such as 550 5.7.1 ... blocked using Spamhaus looks similar and means the opposite. The first is a billable invalid; the second is a free unknown.

Duplicates are free

Gmail dots and +tags are folded before any work happens, so j.doe+leads@gmail.com and jdoe@gmail.com are one person: verified once, billed once. canonical_email shows the key used. Set skipDuplicates: false if you need one output row per input row.

Output

{
"email": "Jane.Roe+leads@Example.com",
"normalized_email": "jane.roe+leads@example.com",
"canonical_email": "jane.roe+leads@example.com",
"domain": "example.com",
"status": "valid",
"state": "Deliverable",
"score": 95,
"reason": "mailbox_exists",
"deliverable": true,
"billed": true,
"free": false,
"role": false,
"disposable": false,
"catch_all": false,
"has_tag": true,
"did_you_mean": null,
"syntax_valid": true,
"mx_found": true,
"smtp_valid": true,
"mx_host": "mx1.example.com",
"mx_records": [{ "host": "mx1.example.com", "priority": 10 }],
"mail_provider": "google",
"verified_at": "2026-08-02T20:12:52.008Z",
"checks": {
"calibrated": true,
"smtp_secure": true,
"smtp_code": 250,
"smtp_enhanced_code": "2.1.5",
"smtp_response": "250 2.1.5 Recipient OK",
"duration_ms": 812
}
}
statusMeaningBilled
validThe mail server confirmed this mailbox exists.Yes
invalidBad syntax, dead domain, null MX, unreachable MX, or the server rejected this recipient by name.Yes
disposableThrowaway mailbox provider.Yes
catch_allThe domain accepts every address. Deliverability is unknowable from outside.No
unknownBlocked, greylisted, timed out, port 25 unreachable, or ambiguous.No

reason carries the detail: mailbox_exists, mailbox_not_found, domain_not_found, no_mx_record, null_mx, mx_host_unresolvable, disposable_domain, accepts_all_recipients, smtp_blocked, greylisted, ambiguous_smtp_reply, smtp_egress_blocked, smtp_proxy_unusable, smtp_unreachable_*, plus the RFC 5322 syntax reasons.

Two outputs, three views

The Actor declares both of its outputs, so the Console Output tab, the GET Run API and any MCP/AI agent can find them without guessing:

outputwherewhat
resultsdefault datasetone item per unique address, all 24 fields
summarykey-value store record SUMMARYcounts by status, decisive rate, duplicates folded, SMTP route used and whether port 25 was reachable

They arrive resolved in the run's output property:

"output": {
"results": "https://api.apify.com/v2/datasets/<id>/items",
"summary": "https://api.apify.com/v2/key-value-stores/<id>/records/SUMMARY"
}

The dataset carries three views, selectable in the Output tab or by appending ?view=<name> to the items URL:

  • Overview — the verdict and the flags that decide whether to send.
  • What you paid forbilled beside status and reason. Sum the billed column and it must equal your invoice. Nothing else in this Store lets you check that.
  • Full diagnostics — every field, with the nested SMTP evidence flattened into checks.* columns.

Views change the Console table and the ?view= API response only. The plain /items URL in output.results always returns complete, unflattened rows, so programmatic consumers never silently lose a field.

Input

Three sources, merged, so you can combine them.

FieldWhat it does
emailsA list pasted into the form.
emailListUrlA CSV or TXT file, uploaded or by public URL. Every address anywhere in the file is picked up, so the column layout does not matter.
inputDatasetId + emailFieldReads addresses straight out of another Actor's run. This is how you clean a scraper's output.
TuningDefaultNotes
proxyUrlnonesocks5://user:pass@host:1080 or http://user:pass@host:8080. Unlocks mailbox checks. Stored as a secret.
verifySmtptrueOff skips the mailbox check and its startup probe entirely.
detectCatchAlltrueTurning it off drops every SMTP score from 95 to 72. Not recommended.
skipDuplicatestrueFold Gmail dots and +tags.
maxConcurrency10Domains in parallel. Addresses on one domain always share a single sequential connection.
smtpTimeoutSecs15Per-command deadline.
perHostDelayMs300Pause between recipients on one server. Lower it and large providers rate-limit you into unknown.
heloDomain, mailFromautoSee below.

Getting better answers out of strict receivers

By default the Actor announces the reverse DNS of its own egress IP, which is what an honest mail server does. Some receivers only answer probes from a host whose forward and reverse DNS agree and that publishes SPF.

If you own a sending domain, set heloDomain to it and mailFrom to a real address on it. Nothing is ever sent — the session ends before DATA, so no message is queued anywhere — but the identity you present decides how many domains answer instead of blocking.

Chaining it after a scraper

{
"inputDatasetId": "<dataset id of the run you want to clean>",
"emailField": "email"
}

Then filter on status = valid, on deliverable = true, or on score >= 90.

Limits, stated plainly

  • No port 25 on Apify. Without a relay you get DNS-level verification, and you are not billed for the mailbox checks that could not run.
  • Yahoo-hosted domains always return catch_all. Yahoo accepts every recipient at RCPT time and bounces later. Not billed.
  • valid means the mailbox existed at probe time, not that your mail will reach the inbox. Reputation, content and authentication decide that.
  • unknown is an outcome, not a failure. Some networks answer no probe from any cloud IP. A different relay changes the answer for some of them.
  • Nothing is stored. Addresses live in your own run's dataset.

Runtime

512 MB. The work is network-bound, not memory-bound. Elapsed time follows the number of distinct domains far more than the number of addresses, because same-domain addresses share one connection.

Build & deploy

Prereqs: Node.js 22+, npm, Apify CLI (npm install -g apify-cli), and an Apify account.

# Install deps and compile TypeScript to dist/
npm install
npm run build
# Run tests
npm test
# Push current source to Apify and build the Actor
apify push

After apify push, the CLI prints the build URL. Follow it to watch the build; when it finishes, the Actor is live at https://apify.com/chloematt/email-verifier.

GitHub integration / auto-deploy

In the Apify console, open this Actor → DeployConnect GitHub repo. Select chloe-matt/chloe-matt-email-verifier, branch master, and enable auto-deploy. After that, every push to master triggers a new build without leaving the terminal.

Verified entrypoints:

  • local dev: npm run start:dev
  • production: node dist/main.js

Apify deploy settings are already in .actor/actor.json and .actor/Dockerfile; apify push reads them from the repo root.