Email Verifier - SMTP, Catch-All & Disposable Detection
Pricing
$2.00 / 1,000 decisive verifications
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
9 days ago
Last modified
Categories
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 5322 | yes | yes |
| Domain / MX / null-MX / unreachable-MX | yes | yes |
| Disposable (11,582 domains) | yes | yes |
| Role, free provider, typo suggestion, de-dupe | yes | yes |
| Mailbox exists? | no — every such address returns unknown, free | yes — valid / 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.
| measure | result |
|---|---|
false invalid verdicts | 0 of 31 (0.00%) |
false disposable verdicts | 0 of 12 |
| non-existent domains caught | 15 of 15 |
| malformed addresses caught | 15 of 15 |
| typos suggested | 4 of 4 |
| typo suggestions on healthy domains (false positives) | 0 of 120 |
| duplicates folded before billing | 4 of 4 |
| decisive (billed) share of this list | 26.06% |
| free of charge | 73.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.
| Situation | What most verifiers return | What this Actor returns |
|---|---|---|
| Domain accepts every recipient | valid, or a silent unknown | catch_all, not billed |
| Our IP is blocked by the receiver | invalid | unknown, not billed |
| Greylisted / rate limited | invalid or unknown | unknown, not billed |
| Port 25 unreachable | invalid, or a crashed run | unknown, not billed |
| Mailbox genuinely rejected | invalid | invalid, 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: trueand 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
unknownand 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}}
status | Meaning | Billed |
|---|---|---|
valid | The mail server confirmed this mailbox exists. | Yes |
invalid | Bad syntax, dead domain, null MX, unreachable MX, or the server rejected this recipient by name. | Yes |
disposable | Throwaway mailbox provider. | Yes |
catch_all | The domain accepts every address. Deliverability is unknowable from outside. | No |
unknown | Blocked, 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:
| output | where | what |
|---|---|---|
results | default dataset | one item per unique address, all 24 fields |
summary | key-value store record SUMMARY | counts 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 for —
billedbesidestatusandreason. Sum thebilledcolumn 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.
| Field | What it does |
|---|---|
emails | A list pasted into the form. |
emailListUrl | A 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 + emailField | Reads addresses straight out of another Actor's run. This is how you clean a scraper's output. |
| Tuning | Default | Notes |
|---|---|---|
proxyUrl | none | socks5://user:pass@host:1080 or http://user:pass@host:8080. Unlocks mailbox checks. Stored as a secret. |
verifySmtp | true | Off skips the mailbox check and its startup probe entirely. |
detectCatchAll | true | Turning it off drops every SMTP score from 95 to 72. Not recommended. |
skipDuplicates | true | Fold Gmail dots and +tags. |
maxConcurrency | 10 | Domains in parallel. Addresses on one domain always share a single sequential connection. |
smtpTimeoutSecs | 15 | Per-command deadline. |
perHostDelayMs | 300 | Pause between recipients on one server. Lower it and large providers rate-limit you into unknown. |
heloDomain, mailFrom | auto | See 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. validmeans the mailbox existed at probe time, not that your mail will reach the inbox. Reputation, content and authentication decide that.unknownis 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 installnpm run build# Run testsnpm test# Push current source to Apify and build the Actorapify 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 → Deploy → Connect 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.