Bulk Email Validator & Checker – MX, Disposable & Typo
Pricing
$0.50 / 1,000 validated emails
Bulk Email Validator & Checker – MX, Disposable & Typo
Bulk email validator: syntax, DNS/MX, disposable, role and typo checks with a 0-100 score. Fast, cheap list cleaning, no SMTP - cannot confirm a mailbox exists.
Pricing
$0.50 / 1,000 validated emails
Rating
0.0
(0)
Developer
Francesco Antonio Russo
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
12 days ago
Last modified
Categories
Security
Limited permissionsShare
Bulk Email Validator & Checker - MX, disposable and typo check (no SMTP)
Clean an email list before you import it, mail it, or pay for a slower verifier. For every address this Actor checks the syntax, looks up the domain's DNS and MX records (including the "null MX" that says a domain accepts no mail), flags disposable domains, role addresses (info@, sales@), free providers (gmail, outlook...) and mistyped domains (gmial.com -> gmail.com), and returns one verdict per address: valid, risky, invalid or unknown, with a 0-100 score and the exact reasons.
It is fast, cheap and deterministic: same list, same answer, no waiting on mail servers. It never connects to a mail server (no SMTP). That is a deliberate choice, and it is also the main limit - read "What it does not do" below before you rely on it.
What it does not do (please read)
- It does not know whether the mailbox exists.
validmeans "the address is well formed, its domain exists and can receive mail, and none of our checks found a problem".jane@gmail.comisvalidwhether or not Jane has that account. Only an SMTP conversation with the mail server can say more, and even that is unreliable (many servers accept everything, or tell nothing). - It does not detect catch-all domains, full mailboxes, spam traps, or addresses that were valid once and were closed.
- It does not send any email and does not contact the owner of any address.
- It does not guess company email patterns and does not find emails: you bring the list.
- The disposable list is maintained by volunteers and can lag behind new services; a domain can also be listed that is no longer disposable.
What you do get is the part that can be known for certain from public data: addresses that can never work (broken syntax, a domain that does not exist, a domain that declares it takes no mail, mail hosts that point to private addresses) and addresses you probably do not want (disposable, role, typo). On a typical scraped or form-collected list that removes the bulk of the bounces at a fraction of the price of an SMTP verifier, and it is a good first pass before one.
What you get
- Syntax (practical RFC 5321/5322): local part, domain, internationalised domains (converted to punycode), 64/254 length limits, dots, spaces, control and invisible characters. Quoted local parts and international local parts are accepted but flagged
risky(see below). - Domain DNS: does the domain exist (
NXDOMAINis certain), does it have MX records, is the MX a null MX (RFC 7505), do the MX hosts resolve to public addresses (not localhost/private), and, when there is no MX, does the domain's own address work as the implicit MX (RFC 5321 section 5.1, reported separately). Each domain is looked up once and the answer is kept for the rest of the run (an MX query plus an address query for the mail host; a failed lookup is tried again after a minute); limited concurrency, a time budget per domain, and one retry. - Honest DNS failures: if DNS does not answer (timeout, SERVFAIL, no network) the status is
unknown, notinvalid, and you are not charged for that row. - Disposable domains: the community list
disposable-email-domains(CC0, see "Data sources"), sub-domains included. - Role accounts and free providers from documented lists (below).
- Typos of well-known providers with a corrected address in
didYouMean, without flagging real domains. - Copy-paste clean-up:
Name <jane@x.com>,<jane@x.com>,mailto:jane@x.com, and punctuation or quotes around an address are taken off and the address inside is judged (flags.extracted, small penalty); your original stays inemail. Two addresses in one entry are not split. - Normalisation: a separate
normalizedfield (trimmed, lowercase, punycode domain); optional Gmail rule (dots and+tagignored). Your original is never modified. - Lists inside dataset rows: if the field holds a list of texts, each text gets its own row (
inputSubIndexis its position). - Duplicates removed (free), only new mode with a privacy-preserving memory, dataset input for big lists, a spending-limit aware run and safe resume after a platform migration.
Who it is for
- Marketers and sales-ops who clean exports before a campaign or a CRM import.
- Developers who validate sign-up lists, newsletter imports or scraped data in a pipeline.
- Anyone who wants to remove the obvious junk before paying for a deep (SMTP) verification.
How to use
Console: paste the addresses in "Emails" (or pick a dataset), press Start, open the dataset (tabs "Verdicts" and "Domain and flags"), export as CSV, Excel or JSON.
API (one call, results back; the synchronous call waits up to 5 minutes, so use it for lists of a few thousand; for more, start the run and fetch the dataset later):
curl -X POST "https://api.apify.com/v2/acts/far003~email-validator-mx-disposable/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \-H "Content-Type: application/json" \-d '{"emails": ["info@apify.com", "john@gmial.com"], "allowRole": false}'
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_API_TOKEN' });const run = await client.actor('far003/email-validator-mx-disposable').call({ emails: ['info@apify.com', 'john@gmial.com'] });const { items } = await client.dataset(run.defaultDatasetId).listItems();const keep = items.filter((row) => row.status === 'valid');
Python:
from apify_client import ApifyClientclient = ApifyClient("YOUR_API_TOKEN")run = client.actor("far003/email-validator-mx-disposable").call(run_input={"emails": ["info@apify.com", "john@gmial.com"]})rows = client.dataset(run["defaultDatasetId"]).list_items().items
No-code: use the Apify integrations of Make, Zapier or n8n (action "Run Actor", then read the dataset), or Google Sheets (export the dataset). A typical flow: scraper Actor, then this Actor with datasetId set to the scraper's dataset, then keep the rows where status is valid.
Input
| Field | Default | What it does |
|---|---|---|
emails | - | List of addresses (up to 100,000 typed in the input). Spaces around an address are ignored. Empty and non-text entries are skipped for free. |
datasetId | - | A dataset of your account to read addresses from (use it for big lists): pick it or paste its ID. Read after the typed list. |
emailField | email | Field with the address in each dataset row; a path such as contact.email works. The field may be a text or a list of texts (one row per text). |
checkMx | true | Look up DNS/MX. Off = syntax and lists only: much faster, but nonexistent domains are not caught, every address scores at most 90, and every address is still judged and charged. |
allowDisposable | false | Treat disposable domains as normal (no penalty; isDisposable is still set). |
allowRole | false | Treat role addresses (info@, admin@...) as normal (no penalty; isRole is still set). |
normalizeGmail | false | In normalized (and for duplicates / "only new"), drop dots and +tag for gmail.com / googlemail.com. |
dedupe | true | Deliver each address once (first occurrence); repeats are skipped and free. Off = every entry is delivered and charged, repeats within the same run included. |
onlyNew | false | Skip addresses validated in earlier runs on your account (free) and remember the new ones. |
maxItems | no limit | Stop after this many delivered rows (free unknown rows count). |
Input errors never start a run: the run ends cleanly (exit code 0) and the SUMMARY record says what to fix. Messages never repeat your addresses.
Output
One dataset row per delivered address, in the order of your input (inputIndex is the position, counting from 0, so you can join the result back to your list).
| Field | Type | Description |
|---|---|---|
inputIndex | integer | Position of the email in your input: the "emails" list first, then the rows of the dataset, counting from 0 (empty rows and duplicates count too). Use it to join the result back to your list. |
inputSubIndex | integer or null | When the field of a dataset row holds a list of texts ("emails": ["a@x.com", "b@y.com"]), each text becomes its own row and this is its position in that list (from 0). Null when the field was a single text. |
email | string | The email exactly as you gave it (only the type is checked; spaces at the ends are kept). Never modified. |
normalized | string or null | The address in the form used for comparison and for duplicates: trimmed, lowercase, domain in punycode. With "normalizeGmail", dots and "+tag" are also removed for gmail.com / googlemail.com addresses. Null when the syntax is invalid. |
status | string | "valid", "risky", "invalid" or "unknown". valid = nothing wrong found (score 80+); risky = deliverable in theory but with a warning (score 1-79); invalid = cannot receive mail (score 0); unknown = DNS did not answer, so no verdict (free). |
score | integer or null | 0-100. invalid = 0. Otherwise 100 minus the penalties of the findings: disposable 60, possible typo 50, quoted local part 40, international local part 30, role account 25, MX missing (A/AAAA fallback only) 25, MX not checked 10; never below 1. Null for unknown. |
reasons | array | Every finding that shaped the verdict, as codes (see the README table). Empty for a clean valid email. Disposable and role are listed only when you did not allow them. |
domain | string or null | The domain of the address, lowercase, in ASCII (internationalised domains as xn--...). Null when the syntax is invalid. |
mx | array | Up to 5 mail hosts of the domain, best priority first. Empty when MX was not checked, when there are none, or when the mail host is the domain itself (usesImplicitMx). |
flags | object | isDisposable, isRole, isFreeProvider (gmail, outlook, yahoo, ...), hasMx (explicit MX records with a usable host; null when not checked or unknown), isNullMx (the domain publishes a null MX and accepts no mail), usesImplicitMx (no MX record: the domain's own address is used, RFC 5321), extracted (the address was taken out of a wrapper such as "Name a@b.com", "mailto:a@b.com" or trailing punctuation; the original stays in email). |
didYouMean | string or null | A corrected full address when the domain looks like a typo of a well-known provider (gmial.com -> gmail.com, .con -> .com). Only for known typos, or for a domain that does not exist and is one edit away from a big provider. Null otherwise. |
checkedAt | string | ISO 8601 time (UTC) of the check. DNS answers are cached for the duration of one run only. |
Statuses and score
| Status | Meaning | Score | Charged |
|---|---|---|---|
valid | Nothing wrong found by the checks above. Says nothing about the mailbox itself. | 80-100 | yes |
risky | Could receive mail, but something is off (disposable, role, typo, unusual syntax...). | 1-79 | yes |
invalid | Cannot receive mail: bad syntax, domain does not exist, null MX, no mail route, unusable MX hosts. | 0 | yes |
unknown | DNS did not answer for this domain, so there is no verdict. Run it again later. | null | no |
The score is 100 minus the points below, never lower than 1 (an invalid is always 0). It is deterministic and the same for every run; valid is 80 and up.
| Reason code | Points off | Meaning |
|---|---|---|
disposable | 60 | The domain (or a parent domain) is on the disposable list. No penalty with "Accept disposable domains". |
possible_typo | 50 | The domain looks like a typo of a well-known provider (see didYouMean). |
role_account | 25 | Role address such as info@ or sales@. No penalty with "Accept role accounts". |
quoted_local_part | 40 | The part before the @ is in quotes ("john doe"@...): legal, but many systems refuse it. |
internationalized_local_part | 30 | Non-ASCII letters before the @ (jürgen@...): needs SMTPUTF8, not supported everywhere. |
implicit_mx_only | 25 | No MX record; the domain's own address would be used (RFC 5321). Many such domains are websites, not mail servers. |
mx_not_checked | 10 | You turned the DNS check off. |
address_extracted | 5 | The address was taken out of a wrapper (Name <a@b.com>, mailto:a@b.com, trailing punctuation); flags.extracted is true and email keeps what you gave. |
Reason codes
Reasons that make an address invalid:
| Reason code | Meaning |
|---|---|
syntax_no_at | There is no "@". |
syntax_multiple_at | More than one "@" outside a quoted local part. |
syntax_empty_local | Nothing before the "@". |
syntax_local_too_long | The part before the "@" is longer than 64 bytes. |
syntax_local_dots | The part before the "@" starts or ends with a dot, or has two dots in a row. |
syntax_local_chars | The part before the "@" has a character that is not allowed. |
syntax_quoted_malformed | A quoted local part ("...") that is not closed correctly. |
syntax_control_chars | Control characters inside the address. |
syntax_whitespace | Spaces or invisible characters inside the address. |
syntax_empty_domain | Nothing after the "@". |
syntax_domain_ip_literal | The domain is an IP address in brackets ([1.2.3.4]); not usable for real mailboxes. |
syntax_domain_trailing_dot | The domain ends with a dot. |
syntax_domain_invalid | The domain has an empty label, a character that is not allowed (letters, digits, hyphen) or a bad hyphen. |
syntax_domain_no_tld | The domain has no dot (for example "user@localhost") or a last label that is not letters (an all-numeric one such as 1.2.3.4 is also refused). |
syntax_domain_too_long | The domain is longer than 253 characters or a label is longer than 63. |
syntax_too_long | The whole address is longer than 254 characters. |
| Reason code | Meaning |
|---|---|
domain_not_found | The domain does not exist (DNS NXDOMAIN). |
null_mx | The domain publishes a null MX (RFC 7505): it says it accepts no mail. |
no_mail_route | The domain exists but has no MX record and no address record: nothing to deliver to. |
mx_unusable | The MX records point only to localhost, private or reserved addresses, or to hosts that do not exist. |
Other codes: dns_unavailable (with status unknown), and the penalty codes in the table above. disposable and role_account appear only when you did not allow them; the flags are always set.
Examples (real output)
Run on 21 October 2026 with the Actor's own code and live DNS. The addresses are made up on real domains.
| Input | Status | Score | Reasons | didYouMean |
|---|---|---|---|---|
info@apify.com | risky | 75 | role_account | - |
john.smith@apify.com | valid | 100 | - | - |
test@mailinator.com | risky | 40 | disposable | - |
john@gmial.com | risky | 1 | possible_typo, disposable, implicit_mx_only | john@gmail.com |
luca@libero.itt | invalid | 0 | domain_not_found, possible_typo | luca@libero.it |
someone@example.com | invalid | 0 | null_mx | - |
user@this-domain-does-not-exist-zq81x.com | invalid | 0 | domain_not_found | - |
bad-address | invalid | 0 | syntax_no_at | - |
"quoted name"@gmail.com | risky | 60 | quoted_local_part | - |
a@co.uk | invalid | 0 | no_mail_route | - |
Jane Roe <jane.roe@gmail.com> | valid | 95 | address_extracted | - |
someone@gmail.it | risky | 50 | possible_typo | someone@gmail.com |
Two complete rows. john@gmial.com has three findings: the typo, the fact that the typo domain is itself on the disposable list, and that it has no MX (only an address record).
[{"inputIndex": 0,"inputSubIndex": null,"email": "info@apify.com","normalized": "info@apify.com","status": "risky","score": 75,"reasons": ["role_account"],"domain": "apify.com","mx": ["aspmx.l.google.com","alt1.aspmx.l.google.com","alt2.aspmx.l.google.com","aspmx2.googlemail.com","aspmx3.googlemail.com"],"flags": {"isDisposable": false,"isRole": true,"isFreeProvider": false,"hasMx": true,"isNullMx": false,"usesImplicitMx": false,"extracted": false},"didYouMean": null,"checkedAt": "2026-10-21T14:38:45.994Z"},{"inputIndex": 10,"inputSubIndex": null,"email": "john@gmial.com","normalized": "john@gmial.com","status": "risky","score": 1,"reasons": ["possible_typo","disposable","implicit_mx_only"],"domain": "gmial.com","mx": [],"flags": {"isDisposable": true,"isRole": false,"isFreeProvider": false,"hasMx": false,"isNullMx": false,"usesImplicitMx": true,"extracted": false},"didYouMean": "john@gmail.com","checkedAt": "2026-10-21T14:38:45.994Z"}]
The SUMMARY record
The key-value store record SUMMARY has counts only, never addresses: rows delivered, valid/risky/invalid/unknown, how many were skipped (empty, not text, duplicates, already checked), how many were not delivered and why (stopReason), DNS statistics, warnings (for example entries that were not text), and the disposable list version. The run ends as failed, with the reason in stopReason, when no entry at all was text (typically a wrong emailField) or when DNS is down. Every input item is accounted for: skipped + resumed + delivered + notDelivered = input.items (a row never reached counts once; the addresses inside an unread list are not counted).
Price
Pay per event: $0.0005 per email with a definitive verdict (valid, risky or invalid) - that is $0.50 per 1,000 and $5 per 10,000. Platform usage is included in the event price.
| Emails with a verdict | Cost |
|---|---|
| 1,000 | $0.50 |
| 10,000 | $5.00 |
| 100,000 | $50.00 |
Free: unknown rows (DNS did not answer), duplicates, empty rows, non-text entries, and addresses skipped by "only new". An address with broken syntax is a verdict (invalid) and is charged: it is a row you asked to be judged. Set the "maximum cost per run" in the run options if you want a hard cap: the Actor reads it, stops when it is reached, delivers exactly what fits and says in the SUMMARY how many were left. Rows are stored first, then charged, in batches of up to 200.
Verifiers that open SMTP connections to mail servers can sometimes tell more about a mailbox, which this Actor cannot; this one costs less because it does less, and says so. If you need to know whether a mailbox exists, run an SMTP verifier on the addresses that survive this pass.
With "check MX" off, every address is still judged and charged: the score tops out at 90 and nonexistent domains are not caught. Use it only when you want the lists and syntax rules alone. Without DNS there is also no proof that a domain cannot receive mail, so the corrections for aol and msn endings and for .co on hotmail, outlook and yahoo are not made; only gmail, googlemail and icloud with a wrong ending are still flagged.
Privacy
Email addresses are personal data. This Actor writes them only to your dataset.
- No address (not even part of one) is written to the run log, status messages or the SUMMARY. Tests capture the log of a run and check this.
- Only new stores in the named key-value store
email-validator-seennothing but salted SHA-256 hashes of the normalized addresses, plus a random salt. A hash cannot be turned back into an address, but anyone holding the store could test a guess against it, so treat the store as private. Delete the store to forget everything. Addresses with statusunknownare not remembered, so they are tried again next time. - It does not contact any mail server and sends nothing to third parties. The only network traffic is DNS queries for the domains (the domain, not the address, is asked).
Speed
Time is set by the number of distinct domains and by the speed of the DNS resolver, not by the number of addresses. The domain lookups run ahead of the rows being delivered (up to 5,000 addresses ahead), with a time budget of 8 seconds per domain: a domain that never answers becomes unknown after 8 seconds and delays only itself.
Measured on 21 October 2026, on a laptop over a home connection (a slow resolver: 10 to 20 DNS queries per second), with local storage:
- 5,000 addresses over 200 real domains: 22 seconds in one run and 48 seconds in another, 0 failed domains in both (466 queries). The difference is the load on the home resolver, not the Actor. On the platform the resolver is faster; this was not measured.
- A silent domain: with a resolver that never answers, one domain costs 8.0 seconds (it cost 21 seconds before the budget existed).
- 1 % silent domains, simulated (8,000 addresses, 400 domains, 4 of them silent, a resolver that answers in 1 ms and the budget scaled to 150 ms): 180 ms against 58 ms without silent domains; with no look-ahead it would be 692 ms. The test that checks this is
test/pipeline.test.js. Silent domains that sit very close to each other in the list overlap their waits; ones that arrive in a long run, far from each other, each cost up to one budget. - The largest list tested end to end is 5,000 addresses; datasets are read in pages, so big lists do not need to fit in the input.
How "only new", resume and the spending limit work
- Only new: the key is the normalized address. Turn on "Treat Gmail dots and +tags as the same address" consistently between runs, or the keys differ.
- Resume: if the platform migrates the run to another server, the new instance reads the rows already in the dataset (by
inputIndex) and continues (an address is identified byinputIndex, orinputIndex.inputSubIndexinside a list); those rows are not delivered or charged again. If the old instance died between storing a batch and charging it, the missing charge is made once on restart. - Spending limit: before each batch the Actor asks how many more events the limit allows. Rows are delivered in input order until the next billable row would not fit; the rest is not delivered, not charged, and counted in the SUMMARY (
input.notDelivered). - DNS outage: if almost every one of the last 50 domains gets no answer the run stops early (status
failedif nothing was delivered) instead of producing a pile ofunknownrows.
Data sources and lists
- Disposable domains: disposable-email-domains,
disposable_email_blocklist.conf, CC0 1.0 (public domain). Copied intovendor/with its licence: 9,913 domains, upstream commitb63df8bof 19 October 2026, fetched 21 October 2026. A domain matches if it, or a parent domain, is on the list (x.mailinator.commatchesmailinator.com). Upstream removed itsallowlist.confon 12 April 2026 ("no longer relevant"); we keep its last version (189 domains, none of which is on the current blocklist) as a safety net: a domain on it is never flagged. We add one override of our own,21cn.com(a large Chinese mailbox provider that the upstream list contains). Some typo domains (for examplegmial.com) are on the upstream list too. - Role accounts, free providers and typos are our own lists, below. They are conservative on purpose: typos are an explicit table, not a fuzzy match, so real small domains are not flagged. One extra rule applies only to a domain that does not exist: if it is one edit away from a big provider (
gmaiil.com), it is suggested.
FAQ
Is valid a guarantee the email will not bounce? No. It means no check failed. The mailbox may not exist, may be full, or the domain may reject unknown users. Use it to remove what is certainly bad, not as a delivery promise.
Why no SMTP check? It is slow, often blocked or misleading (servers accept everything and bounce later, or greylist), and connecting to mail servers from shared IP addresses can get them blacklisted. This Actor stays on public DNS data.
Why is info@company.com "risky"? It is a role address: it goes to a team, and is a poor target for personal outreach. It is not wrong. Turn on "Accept role accounts" to treat it as normal.
Why is "john doe"@example.com or jürgen@example.de "risky"? A quoted part before the @, or non-ASCII letters there, are legal, but many systems refuse them. The score tells you how unusual the address is.
An address I know works is invalid. Check the reason: null_mx, domain_not_found and no_mail_route are about the domain's DNS at the time of the run. If DNS is wrong or recently changed, rerun later; DNS problems that look like timeouts are reported as unknown, never invalid.
Can I put several addresses in one entry? No: each entry is one address. "a@x.com, b@y.com" is judged as a single (invalid) text and is not split.
What does unknown cost? Nothing. Run those addresses again later.
Does it work on private or internal domains? Mail hosts that resolve only to private addresses are reported as mx_unusable: from the public internet they cannot receive mail.
Which DNS does it use? The resolver of the container the run is on. It does not use third-party validation APIs.
Limits
- 100,000 addresses in the
emailsinput; use a dataset for more. - 254 characters per address, 64 bytes before the @, 63 per domain label.
- A domain's DNS answer is kept for the whole run (only a failed lookup is retried after a minute), so a long run does not see DNS changes made after the first lookup of that domain. A domain that needs more than 8 seconds to answer is reported as
unknown. - Quoted local parts and IP-literal domains are not treated as normal mail: quoted ones are
risky, IP literals areinvalid. - A mail host that is a bare IP address in the MX record is accepted if the address is public.