Email Verifier & Validator — Bulk Email List Cleaner, $0.75/1k avatar

Email Verifier & Validator — Bulk Email List Cleaner, $0.75/1k

Pricing

from $0.75 / 1,000 email verifieds

Go to Apify Store
Email Verifier & Validator — Bulk Email List Cleaner, $0.75/1k

Email Verifier & Validator — Bulk Email List Cleaner, $0.75/1k

Clean a contact list end to end: syntax check, normalization, deduplication, disposable and role detection, MX lookup, 0-100 scoring and segmented CSV export (clean / risky / role / invalid). Reads addresses buried in free text. Flat $0.75 per 1,000 on every plan — duplicates are never charged.

Pricing

from $0.75 / 1,000 email verifieds

Rating

0.0

(0)

Developer

Daniele Giovane

Daniele Giovane

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

$0.75 per 1,000 — the same price on every plan, and duplicates are free. Most verifiers on the Store price by plan tier (the most popular one bills $100 per 1,000 on the Free plan, $1.00 Bronze, $0.80 Silver, $0.60 Gold+). This one has a single flat rate for everybody, and because duplicate rows are never charged twice, a list with the typical 18% of duplicates costs about $0.62 per 1,000 rows you upload.

Upload the CSV your CRM exported and get back a deduplicated, normalized, verified and segmented list — plus clean.csv, risky.csv, role.csv and invalid.csv ready to import, and a hygiene report that tells you what was actually wrong with your list.

Why this Actor

  • Deduplication that understands mailbox providers. Gmail dots, +tags (only where the provider implements sub-addressing), googlemail.com, letter case, IDN/punycode, Jane Doe <jane@…> wrappers, mailto: prefixes. Every duplicate is reported with duplicate_of, occurrences and whether a plain string comparison would have found it (exact) or only normalization did (normalized). Search the Store for "email dedup" and you will see why this exists.
  • Files in, files out. CSV, TSV, XLSX, JSON, JSON Lines and plain-text lists — up to 20 files merged and deduplicated together. The email column is auto-detected and always reported, wide CRM exports (hundreds of columns) are read to the end, and include_source_row gives you back every other column, so the cleaned list is still a contact list.
  • It finds addresses buried in text. write to p7@ex.com for info, Referred by ana@acme.io — call first: a notes or comment column full of prose is not a dead end. The address is extracted, verified and deduplicated like any other, the original cell is kept in email_original, and the record says so (note:address_extracted_from_surrounding_text). When a cell holds several addresses the first non-role one wins (Sent by no-reply@sys.com on behalf of mario@acme.commario@acme.com), and punctuation glued to the address is trimmed (contatto: p1@ex.com.Graziep1@ex.com, not a dead ex.com.grazie). Verifiers that only accept one clean address per row return "invalid" for every one of those rows — and bill you for them.
  • Segments you can act on. Every address gets a 0-100 score, an explicit confidence level and a clean / risky / invalid segment, exported as ready-to-import CSV files.
  • Honest about what DNS and SMTP can prove. Nothing is ever upgraded from "we could not check" to "valid", and we charge the full price only when we actually looked something up — see Pricing and The truth about SMTP verification.

Everything runs inside the Actor container: no third-party verification API, no LLM, no data leaving the platform except the DNS queries needed to look up MX records. See Your data (and GDPR).

Input

Provide inline addresses, files, or both — they are merged and deduplicated together.

{
"emails": ["J.Smith+news@Gmail.com", "jsmith@gmail.com", "info@apify.com"],
"sources": [
{ "url": "https://example.com/exports/contacts.csv" },
{ "key": "hubspot-export.xlsx", "store_id": "abcDEF123" },
{ "base64": "bmFtZSxlbWFpbAo...", "filename": "list.csv" }
],
"email_column": "",
"dedup": true,
"normalize": true,
"check_mx": true,
"check_smtp": false,
"detect_disposable": true,
"detect_role": true,
"role_segment": "clean",
"score": true,
"output_segments": true,
"include_source_row": false
}
OptionValuesDefaultWhat it does
emailsarray of stringsInline addresses
sourcesarray of {url} / {base64} / {key,store_id}Up to 20 files, CSV/TSV/XLSX/JSON/JSONL/TXT
email_columnstringautoHeader name of the email column; empty = auto-detect
dedupbooleantruetrue: one record per unique address. false: one record per input row, duplicates flagged
normalizebooleantrueProvider-aware normalization (see below). Domains are lower-cased either way
plus_tag_handlingknown_providers / all / keepknown_providersWhere to strip +tags
check_mxbooleantrueDNS MX lookup, cached per domain
check_smtpbooleanfalseMailbox probe — read the SMTP section first
detect_disposablebooleantrue75,000+ throw-away domains, subdomains included
detect_rolebooleantrueFlags info@, support@, sales-eu@, noreply@ — as information, not as a defect
role_segmentclean / riskycleanWhere role mailboxes land
scorebooleantrue0-100 score, confidence and segment
output_segmentsbooleantrueWrite clean.csv, risky.csv, invalid.csv, role.csv to the run's key-value store
disposable_segmentinvalid / riskyinvalidWhere disposable addresses go
extra_disposable_domainsarray of strings[]Your own additions (max 5000)
disposable_list_urlhttp(s) URLYour own disposable list (one domain per line or a JSON array), merged with the bundled one
include_source_rowbooleanfalseCopy the original row into source_row (max 30 columns, 500 chars per value, 1000 chars per row)
max_emails1–100000100000Safety cap on rows processed and charged
dns_concurrency1–325Parallel MX lookups
dns_timeout_seconds1–305Per-lookup timeout
max_file_size_mb1–10050Bigger files are skipped with a warning
smtp_from_email, smtp_helo_hoststringcontainer defaultsOnly used when check_smtp is on
smtp_timeout_seconds2–6010Per-step SMTP timeout
max_smtp_checks1–200001000SMTP probe budget per run

How the email column is chosen (and how you know)

  1. email_column, if you set it.
  2. Otherwise an unambiguous header name (email, e-mail, email_address, work email, correo, courriel, indirizzo email…). Ambiguous ones (to, mail, recipient) are only considered after those.
  3. Whatever the name says, the data has the last word. The chosen column is sampled: if fewer than 30% of its values look like an address, the column where most values do is used instead — a To,Company,Email file is read from Email, not from To. If no column looks like addresses, nothing is read and nothing is charged, with an explicit warning.
  4. The result is always reported in the log and in the run report: Email column: 'Email' (index 2), chosen by header name 'Email'; 100% of the 200 sampled value(s) look like an address.
  5. A cell that is not an address but contains one has the address extracted (same pattern that selected the column), so a notes column of write to p7@ex.com for info yields 1,200 clean contacts instead of 1,200 invalid rows.
  6. Auto-detection is strict on purpose, but email_column is an instruction: a column you name is read even when only a few of its values look like addresses (the rest come back as invalid rows), as long as it holds at least one. That is how you harvest a free-text notes column.

Malformed files are recovered where possible: a single unbalanced " (the classic way a CSV loses 99% of its rows in silence) is caught by comparing the addresses extracted with the addresses visible in the raw text, then re-parsed ignoring quotes — and the alternative parse is kept only if it yields more real addresses, never because it yields more rows. A valid file with multi-line quoted fields (notes, postal addresses — what Excel, Sheets and HubSpot export every day) is therefore never shredded; it just gets an informational note.

Normalization rules

All applied only when normalize is on, except domain lower-casing which is always applied (domains are case-insensitive by RFC 1035):

RuleExample
Lower-case local partJohn@…john@…
Gmail dotsj.s.mith@gmail.comjsmith@gmail.com
googlemail.com aliasx@googlemail.comx@gmail.com
+tag on providers that support itjohn+news@outlook.comjohn@outlook.com
+tag elsewhere (kept — + is a legal character)sales+eu@acme.example stays
IDN → punycodemario@münchen.demario@xn--mnchen-3ya.de
Wrappers and artifactsJane Doe <jane@x.com>, mailto:jane@x.com, "jane@x.com",jane@x.com

Yahoo is deliberately not on the +tag list: it does not implement + sub-addressing, so a tag there is part of the mailbox name.

Output

One dataset record per address (or per row, with dedup: false):

{
"email_original": "J.Smith+news@Gmail.com",
"email_normalized": "jsmith@gmail.com",
"domain": "gmail.com",
"valid_syntax": true,
"syntax_error": null,
"is_duplicate": false,
"duplicate_of": null,
"duplicate_kind": null,
"occurrences": 3,
"is_disposable": false,
"is_role": false,
"is_free_provider": true,
"suggested_domain": null,
"mx_found": true,
"mx_hosts": ["gmail-smtp-in.l.google.com", "alt1.gmail-smtp-in.l.google.com"],
"mx_status": "mx_found",
"mx_reason": null,
"smtp_result": "skipped",
"smtp_reason": null,
"smtp_code": null,
"confidence": "high",
"score": 100,
"segment": "clean",
"reasons": [],
"source": "contacts.csv",
"row_number": 42,
"source_row": null,
"summary_json": null,
"warnings": []
}

reasons is the audit trail: scoring rules (role_account, mx_lookup_inconclusive:timeout, possible_typo_did_you_mean:gmail.com) plus every normalization step applied, prefixed with note: (note:gmail_dots_removed, note:plus_tag_removed, note:display_name_stripped, note:address_extracted_from_surrounding_text, note:trailing_punctuation_stripped, note:text_boundary_repaired:ex.com, note:skipped_a_role_address_earlier_in_the_text). Nothing is changed silently.

email_original is always the cell as it was in your file — including the surrounding prose when the address was extracted from text; email_normalized is the address itself.

mx_status is one of mx_found, implicit_mx (no MX but an A record — RFC 5321 fallback), null_mx (RFC 7505: the domain refuses all mail), domain_not_found (NXDOMAIN), no_dns_records, timeout, dns_error, budget_exhausted, skipped. The last four mean we could not find out, and the score and confidence say so.

Files written to the run's key-value store:

KeyContent
clean.csv / risky.csv / invalid.csvThe three segments, ready to import
role.csvCross-cutting: every role mailbox, whichever segment it is in — so "clean minus role" is one subtraction away
HYGIENE_REPORTThe full report as JSON

Segments larger than one record are split into clean-2.csv, clean-3.csv, … — never silently truncated. Key-value-store files are used rather than named datasets so a daily scheduled run does not slowly fill your account with storages.

The last dataset record is the hygiene report (email_original: "_summary"), carrying the same columns as every other record plus summary_json — a JSON string, so exporting the dataset to CSV gains one column, not one per report field:

{
"input_rows": 12480, "unique_emails": 10233, "duplicates_found": 2247,
"duplicate_rate_pct": 18.0, "stopped_reason": null,
"charged_emails": 10233, "charged_verified": 10101, "charged_screened": 132,
"estimated_cost_usd_at_list_price": 7.60715,
"syntax": { "valid": 10101, "invalid": 132, "invalid_reasons": { "missing_at_sign": 88 } },
"segments": { "clean": 8410, "risky": 1102, "invalid": 721 },
"confidence": { "high": 9002, "medium": 800, "low": 431 },
"classification": { "disposable": 61, "role": 480, "free_provider": 5210, "corporate": 4891, "typo_suggestions": 27 },
"mx": { "mx_found": 9850, "implicit_mx": 40, "domain_not_found": 180, "timeout": 31 },
"top_domains": [{ "domain": "gmail.com", "count": 3120 }],
"dns": { "unique_domains": 1204, "lookups": 1204 },
"segment_files": { "clean.csv": 8410, "role.csv": 480 },
"options": { "...": "the options this run actually used" },
"warnings": ["..."], "seconds": 41.2
}

Scoring and confidence

Deterministic and auditable — you can recompute any record by hand from this table.

SituationScoreConfidenceSegment
Invalid syntax0highinvalid
Domain does not exist / null MX / no DNS records0highinvalid
Disposable domain5highinvalid (configurable)
SMTP said 5xx (mailbox rejected)0highinvalid
Valid MX record100highclean
No MX but an A record (RFC 5321 implicit MX)70mediumrisky
MX lookup timed out / failed / budget exhausted55lowrisky
check_mx: false60lowrisky
modifier — domain one edit from a big provider (gmial.com)cap 70mediumrisky
modifier — non-ASCII local part (needs SMTPUTF8)−15
modifier — quoted local part−15
modifier — SMTP mailbox accepted+5high
modifier — catch-all domaincap 65mediumrisky
modifier — SMTP inconclusivecap 90

Segments follow the score — clean ≥ 80, risky 40-79, invalid < 40 — with two explicit overrides: disposable addresses go to disposable_segment, role mailboxes to role_segment. Modifiers never push a score below 40: only hard evidence (bad syntax, dead domain, disposable, SMTP 5xx) produces an invalid verdict.

Role accounts do not lose points. ceo@, hello@, press@, partners@ and investors@ are the target of most B2B outreach, not junk; they keep their score, stay in clean by default and are listed in role.csv so you can subtract them if your campaign is a consumer newsletter. Set role_segment: "risky" to move them out of clean.csv. The detector only treats a suffix as part of the role when it is numeric or a region (info.uk@, sales-eu@, support2@) — real people behind a department prefix (it.max@, pr.ben@, hr.zoe@) are not flagged.

What confidence means:

  • high — the evidence is definitive for what it measures: broken syntax, a domain that does not exist, a domain that refuses mail, a disposable domain, or a working MX record. A high-confidence clean means "this domain accepts mail and the address is well formed" — it does not mean "this mailbox exists". Only smtp_result: "deliverable" claims that.
  • medium — indirect evidence: an A record instead of an MX record, a catch-all domain, a likely typo.
  • low — we could not obtain the evidence: DNS timed out, the DNS budget ran out, or check_mx was off. Nothing is guessed in this state.

The truth about SMTP verification

Vendors advertising "98% accuracy" are usually measuring one of two things: MX-level checks (what this Actor does by default) or SMTP RCPT TO probes from a reputation-managed IP pool. Reliable SMTP probing is not something a shared datacenter container can do, and pretending otherwise is how lists get burned:

  • Outbound port 25 is blocked on most cloud networks. A refused connection says nothing about the mailbox.
  • Mailbox providers score the connecting IP. A datacenter IP without a matching PTR record gets greylisted, tarpitted or blanket-accepted whatever the address.
  • Gmail, Outlook and Yahoo accept every recipient on purpose, to defeat address harvesting.
  • Probing at scale looks like a dictionary attack, which is why this Actor probes sequentially and stops after three consecutive connection failures.

So check_smtp is off by default, and when it is on:

smtp_resultMeaning
deliverableThe server accepted the recipient and rejected a random address at the same domain (so it is not a catch-all)
undeliverableThe server rejected the recipient with a 5xx
catch_allThe domain accepts every recipient — the mailbox itself cannot be confirmed
inconclusivePort 25 unreachable, greylisting, 4xx, MAIL FROM refused, budget exhausted, no MX host… smtp_reason says which
skippedcheck_smtp was off

inconclusive is a first-class result, not a failure to hide. Note that catch-all domains are only detected when check_smtp is on — with the default settings a catch-all domain looks like any other domain with a working MX record.

Pricing (pay per event)

EventPriceWhen it is charged
apify-actor-start$0.005Charged automatically by the Apify platform when a run starts. Apify covers the compute cost of the first 5 seconds of every run. Never charged from the Actor code
email-verified$0.00075Once per unique address for which a DNS or SMTP lookup was performed — $0.75 per 1,000
email-screened$0.0002Once per unique address decided with no lookup at all: broken syntax, a disposable domain caught by the local list, or check_mx: false$0.20 per 1,000

We charge the full price only when we actually looked something up.

The $0.005 start fee is charged by the Apify platform on every run, whatever the input turns out to contain — it is what pays for the first 5 seconds of compute, and the Actor never charges it itself. What a run can make free is the work: duplicate rows (charged once, for the first occurrence only), blank cells, rows with no email column, files that could not be downloaded, rows dropped by max_emails, and everything after the run's charge limit is reached — the run stops instead of working for free. A run that reads nothing at all costs the $0.005 start fee and not one address.

And if the file is wrong, you pay $0.005, not for every broken row. If a column is picked but its values are not addresses, nothing is read and no address is charged. If 60% or more of the first 1,000 unique addresses fail syntax validation and the failure has the signature of a parsing accident — values with no @ anywhere, or prose with no address in it — the run stops with a diagnosis instead of billing 100,000 broken rows: a mis-parsed 50k list costs about $0.21, not $37.50.

A list that is simply old and rotten — truncated (john.smith@), malformed or long-dead addresses — is not a parsing accident: that is the job. It is processed to the last row, with a note in the log.

Examples:

  • 10,000 rows with 18% duplicates: $0.005 + 8200 × $0.00075 = $6.16 (≈ $0.62 per 1,000 uploaded rows)
  • 50,000 clean unique rows: $0.005 + 50000 × $0.00075 = $37.51
  • A daily 500-row incremental check: $0.005 + 500 × $0.00075 = $0.38
  • 20,000 rows of junk (wrong file): stopped after the sample, $0.21

Compute is a rounding error — parsing, normalizing, deduplicating and scoring 100,000 rows takes seconds, and the wall-clock time of a run is dominated by DNS latency — so the per-address price is essentially all you pay.

Honest comparison

AlternativeHonest trade-off
Per-address verifier Actors (one address per input item)If your list is already deduplicated and you just want a per-address answer, they are simpler, and on a high plan tier some are cheaper per address. They do not take a CSV, do not deduplicate, do not normalize provider aliases, and do not segment — which is most of the work when the input is a real CRM export.
Commercial verification APIs (ZeroBounce, NeverBounce, …)They own IP pools with mail reputation, so their SMTP-level verdicts are genuinely better than anything a shared datacenter can do. They are also 5-15× the price. Use them for the final pass on a list you are about to mail; use this Actor to remove the 20-40% of rows that never deserved that pass.
Doing it in a spreadsheetFree, and how most people do it. Excel's "Remove duplicates" will not catch j.smith+news@googlemail.com = jsmith@gmail.com, and it cannot check MX records.
Running it yourself (dnspython + a disposable-domain list)Same building blocks. This Actor is the glue: file parsing, column detection and validation, provider rules, caching, budgets, scoring, segment exports, and the caps that stop a 200 MB spreadsheet from eating your afternoon.

We do not claim mailbox-level accuracy we cannot deliver, and we do not price by plan tier.

Your data (and GDPR)

  • You are the data controller. A contact list is personal data. You decide why it is processed; this Actor is a tool you run inside Apify, and Apify is your processor under its Data Processing Addendum. The Actor author is not a party to that processing and never sees your data.
  • Processing is in memory; the output is stored. Rows are parsed in the container and never written anywhere else — but the dataset records and the CSV files this run produces persist in your Apify account under your account's data-retention settings (unnamed storages are deleted after the retention period of your plan; named ones are kept until you delete them). If your retention policy for contact data is shorter, delete the run's storages when you are done.
  • DNS queries expose the domains in your list. Verifying MX records means asking a DNS resolver about example.com for every distinct domain you upload — domains only, never local parts, and one query per unique domain. If that is a problem for your threat model, run with check_mx: false.
  • check_smtp connects to third-party mail servers. It opens unsolicited SMTP conversations with the recipients' mail providers from Apify's IP addresses, on your behalf and under your responsibility. It never sends a message (no DATA command) and it is off by default.
  • No enrichment, no third-party lookups, no LLM. Addresses are never sent to any verification API, enrichment vendor or model. The disposable and role lists are files inside the image; the only outbound traffic is downloading the source URLs you provide, DNS, and — if you enable it — SMTP.
  • No address content in the logs, no telemetry. Logs contain counts, warnings and column names. There is no hidden persistence of any kind.
  • SSRF-guarded fetching. Source URLs that resolve to private, loopback, link-local, multicast, reserved, carrier-grade-NAT (100.64.0.0/10) or IPv4-mapped-IPv6 addresses are refused, including across redirects.

Limits and failure behaviour

Nothing here fails a run: every limit produces a warning in the log and in the hygiene report. Even an empty or unreadable input ends with a SUCCEEDED run and an explicit status message rather than a failure.

LimitValue
Rows per run100,000 (max_emails)
Files per run20
File size50 MB default, 100 MB max
Columns per row1,024 (only the head of a file is held in memory in full)
Distinct domains resolved per run25,000
Total DNS time budget30 minutes, then remaining lookups report budget_exhausted
SMTP probesmax_smtp_checks (default 1,000), sequential
Dataset record8 MB — source_row is dropped (with a warning) before that is reached
Segment CSVsplit into numbered parts, 100,000 rows per segment
Circuit breaker≥60% invalid syntax in the first 1,000 unique addresses and a parsing-accident signature (no @ anywhere in <30% of values, or prose with no address) stops the run. Truncated or malformed addresses are a rotten list, not an accident: processed in full

Malformed CSV, unbalanced quotes, broken ZIP/XLSX, PDFs and images (even renamed to .csv), unreachable URLs, UTF-16/CP1252/Latin-1 encodings, spreadsheet formulas with no cached value, and files with no email column all produce a warning and let the rest of the run continue.

FAQ

Does it work with a Google Sheets / HubSpot / Mailchimp export? Yes — export as CSV or XLSX and pass the URL, or upload it to a key-value store and pass { "key": "export.csv" }. Wide exports with hundreds of columns are supported. Set include_source_row: true to get all the other columns back alongside the verdict.

My addresses are inside a notes/comments column, mixed with text. Is that a problem? No. Point email_column at it (or let auto-detection find it): each address is extracted from the surrounding text, verified and deduplicated like any other, and email_original keeps the full original cell so you can still see the context. If a cell contains more than one address, the first non-role one is taken.

The run says "Could not identify an email column" but my addresses ARE in there (in a notes column, only in some rows). Auto-detection only picks a column when most of its values look like addresses, so a notes column where one row in five carries an address is skipped — nothing is read and nothing is charged. Set email_column to that column's name: an explicitly named column is read even when few values look like addresses, and every address inside the text is extracted. Rows with no address in them come back as invalid (and are charged at the email-screened rate of $0.0002).

How do I merge several lists? Put them all in sources: they are read in order, deduplicated across files, and source + row_number in each record tell you where each address came from.

Why is my whole list risky when I turn check_mx off? By design: without an MX lookup there is no evidence a domain can receive mail, so the best available verdict is syntax-level. Nothing reaches clean without at least an MX check. Those addresses are billed at the email-screened rate.

Why is a valid-looking address risky with mx_status: timeout? Because the DNS server did not answer in time. We report that instead of guessing. Re-run with a higher dns_timeout_seconds, or lower dns_concurrency if your resolver is rate-limiting.

How complete is the disposable list? It bundles 75,000+ domains merged from two maintained open-source feeds plus our own additions, matched on subdomains too. No list is ever complete: extra_disposable_domains adds your own, and disposable_list_url lets you plug in a feed that updates faster than this Actor's releases.

Why is info@company.com in clean.csv? Because it is a real, deliverable mailbox, and for B2B outreach it is often the one you want. It is flagged (is_role: true), exported to role.csv, and you can move all role addresses to risky with role_segment: "risky".

Can I get only the clean addresses? Download clean.csv from the run's key-value store, or filter the dataset on segment / score.

Does it send any email? No. Even with check_smtp on, the conversation stops at RCPT TO and always ends with QUIT; no DATA, no message is ever transmitted.

Attribution. The bundled disposable-domain list merges disposable/disposable-email-domains and FGRibreau/mailchecker, both MIT licensed; the licences travel with the list inside the image.

You may also need

Same author, same principles: you pay for work that was actually done, nothing is stored outside your own run, and no third-party API ever sees your data.