# Bulk Email Validator & Checker – MX, Disposable & Typo (`far003/email-validator-mx-disposable`) Actor

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.

- **URL**: https://apify.com/far003/email-validator-mx-disposable.md
- **Developed by:** [Francesco Antonio Russo](https://apify.com/far003) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.50 / 1,000 validated emails

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 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.** `valid` means "the address is well formed, its domain exists and can receive mail, and none of our checks found a problem". `jane@gmail.com` is `valid` whether 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 (`NXDOMAIN` is 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`, not `invalid`, 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 in `email`. Two addresses in one entry are not split.
- **Normalisation**: a separate `normalized` field (trimmed, lowercase, punycode domain); optional Gmail rule (dots and `+tag` ignored). Your original is never modified.
- **Lists inside dataset rows**: if the field holds a list of texts, each text gets its own row (`inputSubIndex` is 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):

```bash
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**:

```js
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**:

```python
from apify_client import ApifyClient

client = 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).

```json
[
  {
    "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-seen` nothing 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 status `unknown` are 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 by `inputIndex`, or `inputIndex.inputSubIndex` inside 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 `failed` if nothing was delivered) instead of producing a pile of `unknown` rows.

### Data sources and lists

- **Disposable domains**: [disposable-email-domains](https://github.com/disposable-email-domains/disposable-email-domains), `disposable_email_blocklist.conf`, **CC0 1.0** (public domain). Copied into `vendor/` with its licence: **9,913 domains**, upstream commit `b63df8b` of 19 October 2026, fetched 21 October 2026. A domain matches if it, or a parent domain, is on the list (`x.mailinator.com` matches `mailinator.com`). Upstream removed its `allowlist.conf` on 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 example `gmial.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.

<details><summary>Role accounts (matched on the whole part before the @, ignoring case, <code>+tag</code> and <code>.</code> <code>-</code> <code>_</code>)</summary>

118 words: `abuse`, `accounting`, `accounts`, `accountsreceivable`, `accountspayable`, `admin`, `administrator`, `admissions`, `advertising`, `alerts`, `all`, `billing`, `bookings`, `booking`, `bounce`, `bounces`, `careers`, `compliance`, `contact`, `contactus`, `customercare`, `customerservice`, `customersupport`, `devnull`, `dns`, `donotreply`, `enquiries`, `enquiry`, `events`, `everyone`, `feedback`, `finance`, `ftp`, `general`, `help`, `helpdesk`, `hello`, `hostmaster`, `hr`, `info`, `information`, `inquiries`, `inquiry`, `invoice`, `invoices`, `it`, `jobs`, `legal`, `list`, `mail`, `mailer`, `maildaemon`, `mailerdaemon`, `mailroom`, `marketing`, `media`, `news`, `newsletter`, `newsletters`, `noc`, `noreply`, `notifications`, `notification`, `office`, `operations`, `order`, `orders`, `owner`, `payroll`, `postmaster`, `press`, `privacy`, `purchasing`, `recruiting`, `recruitment`, `reception`, `registrar`, `reply`, `reservations`, `root`, `sales`, `security`, `service`, `services`, `subscribe`, `support`, `sysadmin`, `team`, `tech`, `unsubscribe`, `usenet`, `uucp`, `webmaster`, `www`, `amministrazione`, `assistenza`, `commerciale`, `contabilita`, `segreteria`, `ufficio`, `vendite`, `fatture`, `fatturazione`, `privato`, `bonjour`, `comptabilite`, `commercial`, `accueil`, `kontakt`, `vertrieb`, `buchhaltung`, `verkauf`, `ventas`, `soporte`, `contabilidad`, `administracion`, `contacto`, `oficina`

</details>

<details><summary>Free mailbox providers (<code>isFreeProvider</code>)</summary>

115 domains: `gmail.com`, `googlemail.com`, `outlook.com`, `outlook.it`, `outlook.fr`, `outlook.de`, `outlook.es`, `hotmail.com`, `hotmail.it`, `hotmail.fr`, `hotmail.de`, `hotmail.es`, `hotmail.co.uk`, `live.com`, `live.it`, `live.fr`, `live.co.uk`, `msn.com`, `yahoo.com`, `yahoo.it`, `yahoo.fr`, `yahoo.de`, `yahoo.es`, `yahoo.co.uk`, `yahoo.co.jp`, `yahoo.com.br`, `yahoo.ca`, `yahoo.in`, `ymail.com`, `rocketmail.com`, `icloud.com`, `me.com`, `mac.com`, `aol.com`, `aim.com`, `gmx.com`, `gmx.de`, `gmx.net`, `gmx.at`, `gmx.ch`, `gmx.fr`, `web.de`, `libero.it`, `virgilio.it`, `tiscali.it`, `alice.it`, `tin.it`, `email.it`, `inwind.it`, `iol.it`, `fastwebnet.it`, `proton.me`, `protonmail.com`, `protonmail.ch`, `pm.me`, `tutanota.com`, `tuta.com`, `tutamail.com`, `tuta.io`, `hey.com`, `fastmail.com`, `fastmail.fm`, `zoho.com`, `zohomail.com`, `mail.com`, `email.com`, `usa.com`, `mail.ru`, `inbox.ru`, `list.ru`, `bk.ru`, `yandex.com`, `yandex.ru`, `ya.ru`, `qq.com`, `163.com`, `126.com`, `21cn.com`, `sina.com`, `naver.com`, `daum.net`, `hanmail.net`, `rediffmail.com`, `orange.fr`, `wanadoo.fr`, `free.fr`, `laposte.net`, `sfr.fr`, `neuf.fr`, `bbox.fr`, `t-online.de`, `freenet.de`, `arcor.de`, `btinternet.com`, `sky.com`, `virginmedia.com`, `talktalk.net`, `comcast.net`, `verizon.net`, `att.net`, `sbcglobal.net`, `cox.net`, `bellsouth.net`, `charter.net`, `earthlink.net`, `terra.com.br`, `uol.com.br`, `bol.com.br`, `ig.com.br`, `telenet.be`, `skynet.be`, `ziggo.nl`, `planet.nl`, `hetnet.nl`, `xs4all.nl`

</details>

<details><summary>Known domain typos (<code>didYouMean</code>)</summary>

77 domains: `gmial.com` -> gmail.com, `gmali.com` -> gmail.com, `gamil.com` -> gmail.com, `gmai.com` -> gmail.com, `gmal.com` -> gmail.com, `gmaill.com` -> gmail.com, `gmil.com` -> gmail.com, `gnail.com` -> gmail.com, `gmeil.com` -> gmail.com, `gmail.comm` -> gmail.com, `gmail.cm` -> gmail.com, `gmail.om` -> gmail.com, `gmail.vom` -> gmail.com, `gmail.xom` -> gmail.com, `gmaul.com` -> gmail.com, `gmaiil.com` -> gmail.com, `gmailcom.com` -> gmail.com, `gmail.con` -> gmail.com, `gmail.cmo` -> gmail.com, `gmail.ocm` -> gmail.com, `gogglemail.com` -> googlemail.com, `hotmial.com` -> hotmail.com, `hotmai.com` -> hotmail.com, `hotmal.com` -> hotmail.com, `hotmil.com` -> hotmail.com, `hotmaill.com` -> hotmail.com, `homail.com` -> hotmail.com, `hotnail.com` -> hotmail.com, `hitmail.com` -> hotmail.com, `hotmailcom.com` -> hotmail.com, `hotmail.comm` -> hotmail.com, `hotmail.cm` -> hotmail.com, `hotmali.com` -> hotmail.com, `hotmai.it` -> hotmail.it, `hotmial.it` -> hotmail.it, `hotmal.it` -> hotmail.it, `yahooo.com` -> yahoo.com, `yaho.com` -> yahoo.com, `yahho.com` -> yahoo.com, `yhoo.com` -> yahoo.com, `yaoo.com` -> yahoo.com, `yahoo.comm` -> yahoo.com, `yahoo.cm` -> yahoo.com, `yahou.com` -> yahoo.com, `yahooo.it` -> yahoo.it, `yaho.it` -> yahoo.it, `yahoo.itt` -> yahoo.it, `outlok.com` -> outlook.com, `outloook.com` -> outlook.com, `outllook.com` -> outlook.com, `outlookk.com` -> outlook.com, `outlool.com` -> outlook.com, `outook.com` -> outlook.com, `outlook.comm` -> outlook.com, `outlook.cm` -> outlook.com, `outlock.com` -> outlook.com, `otlook.com` -> outlook.com, `iclod.com` -> icloud.com, `icoud.com` -> icloud.com, `iclould.com` -> icloud.com, `icloud.comm` -> icloud.com, `icloud.cm` -> icloud.com, `icloude.com` -> icloud.com, `iclound.com` -> icloud.com, `libero.itt` -> libero.it, `libreo.it` -> libero.it, `virgilio.itt` -> virgilio.it, `tiscali.itt` -> tiscali.it, `protonmial.com` -> protonmail.com, `protonmal.com` -> protonmail.com, `protonmail.comm` -> protonmail.com, `aol.comm` -> aol.com, `msn.comm` -> msn.com, `live.comm` -> live.com, `gmx.dee` -> gmx.de, `gmx.nett` -> gmx.net, `web.dee` -> web.de.

Wrong last labels corrected on any domain (20): .con -> .com, .cmo -> .com, .ocm -> .com, .vom -> .com, .xom -> .com, .comm -> .com, .coom -> .com, .cim -> .com, .clm -> .com, .cpm -> .com, .comn -> .com, .vcom -> .com, .nte -> .net, .ent -> .net, .ner -> .net, .nett -> .net, .ogr -> .org, .rog -> .org, .orgg -> .org, .itt -> .it.

Wrong endings on a brand: `gmail`, `googlemail`, `icloud` have no national mail domains, so `gmail.it`, `gmail.co` or `icloud.net` are suggested as `.com` (and are never `valid`). `aol`, `msn` do receive mail on other endings (`aol.fr`, `msn.cn`: left alone), and `hotmail`, `outlook`, `yahoo` have national domains (`yahoo.it`, `hotmail.fr`) and `outlook.co` works too; for these brands the `.com` is suggested only when the other ending cannot receive mail at all (`aol.xyz` that does not exist, `yahoo.co` with a null MX).

</details>

### 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 `emails` input; 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 are `invalid`.
- A mail host that is a bare IP address in the MX record is accepted if the address is public.

# Actor input Schema

## `emails` (type: `array`):

The addresses to check, one per entry. Up to 100,000 here; for bigger lists put them in a dataset and use "Dataset ID". Spaces around an address are ignored, empty and non-text entries are skipped for free.

## `datasetId` (type: `string`):

Read the addresses from a dataset of your own Apify account instead of, or in addition to, the list above. Pick it from the list or paste its ID. Rows are read after the typed list. A field that holds a list of texts gives one row per text.

## `emailField` (type: `string`):

Name of the field that holds the address in each dataset row. A path with dots such as "contact.email" works. The field may be a text or a list of texts; rows where it is missing or not text are skipped for free.

## `checkMx` (type: `boolean`):

Look up the domain: does it exist, does it accept mail (MX, null MX, address fallback)? Turn it off for a syntax-and-lists-only check: much faster, but a made-up domain such as nope.example-xyz.com is not caught, and every address is still judged and charged (score at most 90).

## `allowDisposable` (type: `boolean`):

By default an address on a disposable-email domain (mailinator.com and the like) is "risky" with a score cut by 60. Turn this on to treat them as normal; the isDisposable flag is still set.

## `allowRole` (type: `boolean`):

By default a role address (info@, admin@, sales@, noreply@...) is "risky" with a score cut by 25: it goes to a team, not a person. Turn this on to treat them as normal; the isRole flag is still set.

## `normalizeGmail` (type: `boolean`):

Gmail ignores dots and anything after a plus sign in the part before the @. When on, "j.doe+news@gmail.com" and "jdoe@gmail.com" get the same normalized value, so they count as duplicates (and as one address for "Only new"). The original address is never changed. Off by default because it is a Gmail-specific rule.

## `dedupe` (type: `boolean`):

Deliver each address once (the first occurrence). The repeats are skipped and free; the SUMMARY counts them. Turn off to get one row per input entry: repeats within the same run are then delivered and charged like any other address.

## `onlyNew` (type: `boolean`):

Skip addresses already validated in earlier runs of this Actor on your account, and remember the ones validated now. Only a salted SHA-256 hash of each normalized address is stored (in the key-value store "email-validator-seen"), never the address. Skipped emails are free. Emails with status "unknown" are not remembered, so they are tried again next time.

## `maxItems` (type: `integer`):

Stop after this many rows have been delivered (free "unknown" rows count). Leave empty for no limit. The run also stops, without error, when your spending limit is reached.

## Actor input object example

```json
{
  "emails": [
    "info@apify.com",
    "test@mailinator.com",
    "john@gmial.com"
  ],
  "emailField": "email",
  "checkMx": true,
  "allowDisposable": false,
  "allowRole": false,
  "normalizeGmail": false,
  "dedupe": true,
  "onlyNew": false
}
```

# Actor output Schema

## `emails` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "emails": [
        "info@apify.com",
        "test@mailinator.com",
        "john@gmial.com",
        "jane.doe@gmail.com",
        "someone@example.com"
    ],
    "emailField": "email",
    "checkMx": true,
    "allowDisposable": false,
    "allowRole": false,
    "normalizeGmail": false,
    "dedupe": true,
    "onlyNew": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("far003/email-validator-mx-disposable").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "emails": [
        "info@apify.com",
        "test@mailinator.com",
        "john@gmial.com",
        "jane.doe@gmail.com",
        "someone@example.com",
    ],
    "emailField": "email",
    "checkMx": True,
    "allowDisposable": False,
    "allowRole": False,
    "normalizeGmail": False,
    "dedupe": True,
    "onlyNew": False,
}

# Run the Actor and wait for it to finish
run = client.actor("far003/email-validator-mx-disposable").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "emails": [
    "info@apify.com",
    "test@mailinator.com",
    "john@gmial.com",
    "jane.doe@gmail.com",
    "someone@example.com"
  ],
  "emailField": "email",
  "checkMx": true,
  "allowDisposable": false,
  "allowRole": false,
  "normalizeGmail": false,
  "dedupe": true,
  "onlyNew": false
}' |
apify call far003/email-validator-mx-disposable --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,far003/email-validator-mx-disposable"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/2a8kGeA0B5kAtI9GB/builds/iy6BRSquqPckFe2AG/openapi.json
