# Bulk Email Validator: MX, Disposable, Typo and Role Check (`joesimo/email-validator`) Actor

Cut bounces before you send: bulk email validation with syntax, MX and null MX, dead domains, disposable and role addresses, free providers, typos (gmial.com) with fixes, SPF and DMARC. Status valid, risky, invalid or unknown with reasons. DNS only, no SMTP, so mailboxes are not confirmed.

- **URL**: https://apify.com/joesimo/email-validator.md
- **Developed by:** [Joe Simo](https://apify.com/joesimo) (community)
- **Categories:** Lead generation, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 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

## Email Validator: Bulk MX, Disposable and Role Check

**Clean an email list before you send to it, and cut bounces from dead domains, typos and throwaway addresses.** Paste addresses, upload a list or point the Actor at a dataset column, and get one row per address with a clear status (**valid**, **risky**, **invalid** or **unknown**), the exact reasons, and the DNS facts behind them.

Every address gets checked for:

- **Syntax**: RFC 5321/5322 rules that matter in practice, with plain reasons such as `missing_at_sign` or `invalid_dots`. Handles `Jane Doe <jane@acme.com>`, `mailto:` links, stray quotes and punctuation, and international domains (IDN).
- **Domain and mail servers**: does the domain exist (NXDOMAIN), does it have MX records, a **null MX** (RFC 7505, "this domain takes no email"), or only an A record fallback. Optionally checks that the listed mail server host names actually resolve.
- **Disposable domains**: about 10,000 throwaway domains (mailinator.com, yopmail.com, 10minutemail.com ...), including their subdomains.
- **Role accounts**: info@, sales@, support@, admin@, noreply@ and about 140 more shared mailboxes.
- **Free providers**: Gmail, Outlook, Yahoo, iCloud, Proton, GMX, Mail.ru, QQ and about 160 more.
- **Typos**: `gmial.com`, `hotmial.com`, `gmail.con` and similar, with a suggested fix (`jane@gmail.com`).
- **SPF and DMARC**: whether the domain publishes them, and the DMARC policy (`none`, `quarantine`, `reject`).
- **Mail provider**: Google Workspace, Microsoft 365, Yahoo, Proofpoint, Mimecast, Zoho and others, detected from MX records.

**Important and honest:** this Actor works at the DNS level only. It never connects to mail servers (no SMTP probing), so it **cannot confirm that a specific mailbox exists** and cannot detect catch-all domains. `valid` means "the address is well formed and its domain is set up to receive email", not "this person will get your message". Every row says so with `mailboxChecked: false` and `catchAll: null`. In exchange it is fast, cheap, does not touch anyone's mail server, and cannot get your IP blocklisted.

### Quick start

1. Click **Try for free**, paste your addresses into **Emails** and press **Start**.
2. Or call the API with an empty input `{}`. That checks these 6 demo addresses:

```json
{
    "emails": ["john.doe@gmail.com", "info@apify.com", "someone@mailinator.com", "jane@gmial.com", "bad.address@", "user@example.com"]
}
```

On Nov 1, 2026 this run checked 6 addresses in about 1 second for 6 x $0.001 = $0.006: 1 valid (`john.doe@gmail.com`), 3 risky (`info@apify.com` role account, `someone@mailinator.com` disposable, `jane@gmial.com` typo) and 2 invalid (`bad.address@` missing domain, `user@example.com` null MX). The typo row, shortened:

```json
{
    "email": "jane@gmial.com",
    "status": "risky",
    "reasons": ["disposable_domain", "no_mx_a_fallback", "possible_typo"],
    "suggestion": "jane@gmail.com",
    "domain": "gmial.com",
    "syntaxValid": true,
    "domainExists": true,
    "hasMx": false,
    "isDisposable": true,
    "mailboxChecked": false,
    "catchAll": null
}
```

### What you can use it for

- **Clean signup forms and CRM exports**: drop dead domains, typos and throwaway addresses before they hurt deliverability.
- **Pre-filter before a paid SMTP verifier**: remove the obvious invalid and disposable rows first, so you pay the expensive per-mailbox check only for what is left.
- **Lead list hygiene**: flag role accounts and free mailboxes in B2B lists, and see which companies run Google Workspace or Microsoft 365.
- **Fraud and abuse screening**: spot disposable domains and domains with no mail setup on new accounts.
- **Enrich datasets from other Actors**: point it at any Apify dataset and the field that holds the email.

### Why this Actor

- **$1 per 1,000 emails**, pay only for what is checked. Duplicates are removed before charging.
- **Fast**: domains are looked up once and cached, with parallel DNS queries, timeouts, retries and a DNS over HTTPS second opinion for flaky domains.
- **Clear output**: one status, machine readable reason codes, and a plain English `reasonText` for every row.
- **Your rules**: decide whether role accounts, free providers and disposable domains count as risky or invalid.
- **No proxy, no browser, no SMTP**: nothing that can get blocked or blocklisted.

### Input

Give addresses in any combination of three ways: a list, a pasted block of text, or a dataset.

```json
{
    "emails": ["john.doe@gmail.com", "info@apify.com", "someone@mailinator.com", "jane@gmial.com", "bad.address@", "user@example.com"],
    "emailsText": "Jane Doe <jane@acme.com>, bob@yahoo.com\nsales@shop.example",
    "roleAsRisky": true,
    "disposableStatus": "risky",
    "freeAsRisky": false
}
```

| Field | Default | What it does |
|---|---|---|
| `emails` | | List of addresses, one per entry. |
| `emailsText` | | Paste a whole column or text block. Split on new lines, commas, semicolons and tabs. Tokens without an @ (like a CSV header) are skipped and counted. |
| `datasetId` | | Read addresses from an existing Apify dataset (ID or name), for example the output of another Actor. |
| `datasetField` | `email` | Field that holds the address. Dot paths work (`contact.email`) and array values are expanded. |
| `maxItems` | 0 (no limit) | Check at most this many addresses (after removing duplicates). |
| `dedupe` | true | Remove duplicates (case insensitive) before checking, so you never pay twice for the same address. |
| `roleAsRisky` | true | Role accounts (info@, sales@ ...) get status risky. Off: they can be valid, the flag stays. |
| `disposableStatus` | `risky` | Status for disposable domains: `risky` or `invalid`. |
| `freeAsRisky` | false | Free consumer providers (gmail.com ...) get status risky. Useful for B2B lists. |
| `checkMxHosts` | true | Also check that the listed mail server host names resolve. |
| `checkSpfDmarc` | true | Look up SPF and DMARC records (informational, does not change status). |
| `includeSpfRecord` | false | Add the raw SPF record text to each row. |
| `dnsMode` | `udp` | `udp` uses plain DNS (fastest). `doh` uses DNS over HTTPS (Cloudflare and Google). |
| `dnsServers` | `1.1.1.1`, `8.8.8.8` | Resolvers for plain DNS. Leave empty to use the system resolver. |
| `dohFallback` | true | When a plain DNS lookup still fails after all retries, ask Google DNS over HTTPS once before returning unknown. |
| `dnsConcurrency` | 50 | Parallel DNS queries. |
| `dnsTimeoutSecs` | 4 | Timeout per DNS query. |
| `dnsRetries` | 2 | Retries on timeouts and server failures, rotating resolvers. |

Before checking anything, the Actor runs a DNS self test (gmail.com must return Google mail servers). If the configured resolver is blocked it falls back to the system resolver, then to DNS over HTTPS. If nothing works, the run fails **before anything is charged**.

### Output

One dataset row per checked address, in input order:

```json
{
    "email": "info@apify.com",
    "normalizedEmail": "info@apify.com",
    "status": "risky",
    "reasons": ["role_account"],
    "reasonText": "Role or shared mailbox (info@, sales@, support@ ...), often a group inbox with low engagement.",
    "suggestion": null,
    "localPart": "info",
    "domain": "apify.com",
    "syntaxValid": true,
    "domainExists": true,
    "hasMx": true,
    "nullMx": false,
    "mxFallbackToA": false,
    "mxRecords": [{ "exchange": "aspmx.l.google.com", "priority": 1 }, { "exchange": "alt1.aspmx.l.google.com", "priority": 5 }],
    "mailProvider": "Google",
    "isDisposable": false,
    "isRoleAccount": true,
    "isFreeProvider": false,
    "plusTag": null,
    "hasSpf": true,
    "hasDmarc": true,
    "dmarcPolicy": "reject",
    "mailboxChecked": false,
    "catchAll": null,
    "dnsErrorCode": null,
    "checkedAt": "2026-10-29T12:18:39.013Z"
}
```

A typo row looks like `"email": "jane@gmial.com", "status": "risky", "reasons": ["disposable_domain", "no_mx_a_fallback", "possible_typo"], "suggestion": "jane@gmail.com"`.

#### Statuses

| Status | Meaning | Typical action |
|---|---|---|
| `valid` | Well formed, the domain exists and is set up to receive mail, no risk flags. | Keep. |
| `risky` | Deliverable domain, but something is off: disposable, role account, typo suspect, no MX (A record fallback), quoted or non-ASCII local part, or free provider if you turned that on. | Review, or segment. |
| `invalid` | Bad syntax, the domain does not exist, has a null MX, has no mail server at all, or its mail servers do not resolve. | Remove. |
| `unknown` | DNS failed after retries (timeout or SERVFAIL). Usually a broken or temporarily unreachable domain. | Retry later. |

#### Reason codes

| Code | Status | Meaning |
|---|---|---|
| `empty`, `contains_whitespace`, `missing_at_sign`, `multiple_at_signs`, `missing_local_part`, `missing_domain`, `invalid_dots`, `invalid_characters`, `invalid_local_part`, `local_part_too_long`, `address_too_long` | invalid | Syntax problems in the address. |
| `ip_address_domain`, `invalid_domain`, `domain_missing_tld`, `domain_too_long`, `invalid_tld`, `reserved_domain` | invalid | Domain part is malformed or reserved (.test, .local, .invalid, localhost ...). |
| `domain_not_found` | invalid | The domain does not exist (NXDOMAIN). |
| `null_mx` | invalid | The domain publishes a null MX (RFC 7505): it accepts no email. |
| `no_mail_server` | invalid | No MX and no A/AAAA record. |
| `mx_host_not_found` | invalid | None of the listed mail servers resolve. |
| `mx_localhost` | invalid | Mail servers point to localhost or an unroutable address. |
| `disposable_domain_invalid` | invalid | Disposable domain, when `disposableStatus` is `invalid`. |
| `disposable_domain` | risky | Disposable (throwaway) domain. |
| `role_account` | risky | Role or shared mailbox. |
| `possible_typo` | risky | Domain looks like a typo of a popular provider; see `suggestion`. |
| `no_mx_a_fallback` | risky | No MX record; mail would go to the domain A record. Often not a real mail setup. |
| `quoted_local_part`, `non_ascii_local_part` | risky | Technically allowed but rejected by many systems. |
| `free_provider` | risky | Only when `freeAsRisky` is on. |
| `dns_error`, `mx_host_dns_error` | unknown | DNS lookups failed after retries. |

#### Field reference

| Field | Description |
|---|---|
| `email` | The value as supplied. |
| `normalizedEmail` | Cleaned address with a lowercase, ASCII (punycode) domain. The local part keeps its case. |
| `status`, `reasons`, `reasonText` | Verdict, reason codes, and plain English explanation. |
| `suggestion` | Corrected address when the domain looks like a typo, else null. |
| `localPart`, `domain` | The two halves of the address. |
| `syntaxValid` | Passed the syntax checks. |
| `domainExists` | The domain resolves in DNS. Null if not checked or DNS failed. |
| `hasMx`, `nullMx`, `mxFallbackToA` | MX record found, null MX found, or no MX but an A/AAAA record. |
| `mxRecords` | Up to 5 mail servers, best priority first. |
| `mailProvider` | Detected mail host (Google, Microsoft, Yahoo, Proofpoint ...), or null. |
| `isDisposable`, `isRoleAccount`, `isFreeProvider` | Flags, independent of how you set the status rules. |
| `plusTag` | Sub-address tag, for example `promo` in `sales+promo@acme.com`. |
| `hasSpf`, `hasDmarc`, `dmarcPolicy` | Email authentication records of the domain. `spfRecord` is added when `includeSpfRecord` is on. |
| `mailboxChecked`, `catchAll` | Always `false` and `null`: this Actor does not open SMTP connections. |
| `dnsErrorCode` | DNS error code when status is unknown. |
| `checkedAt` | ISO time of the check. |

#### Run summary

The key value store record `OUTPUT` holds a summary: emails supplied, duplicates removed, emails checked, counts per status and per reason, unique domains, DNS statistics (resolver used, queries, cache hits, retries, timeouts) and run time. The dataset also has an `Overview` view and a `DNS details` view in Console.

### Pricing

Pay per event, no subscription:

| Event | Price |
|---|---|
| Email checked (one dataset row) | $0.001 ($1 per 1,000) |
| Actor start | $0.00005 per run (Apify default) |

Duplicates removed by `dedupe` are not charged. If you set a **maximum cost per run**, the Actor checks only as many addresses as fit, stops cleanly, and reports how many were left in `notCheckedBecauseOfChargeLimit`.

Rough cost: 10,000 addresses cost about $10. In our cloud test, 10,000 addresses across about 1,000 different domains finished in under a minute at the default 256 MB memory. Lists with fewer distinct domains are faster.

### Limits and good to know

- **No mailbox check.** DNS cannot tell whether `john@acme.com` exists. If you need that, run the cleaned list through an SMTP verifier; this Actor makes that list shorter and cheaper.
- **No catch-all detection** for the same reason.
- **Disposable lists are never complete.** New throwaway domains appear every day. The list is updated with new Actor versions.
- **Role and free provider lists** cover the common cases, not every one in the world.
- **`unknown` is rare** and usually means the domain's own DNS is broken. Rerun those rows later.
- Domains are cached within a run, so lists with many addresses at the same company are very fast.

### FAQ

**Is a `valid` address guaranteed to be deliverable?**
No. It means the address is well formed and its domain accepts mail according to DNS. The mailbox may still not exist, be full, or bounce. Anyone who promises more without SMTP access is guessing.

**Why not do SMTP checks?**
SMTP probing is slow, often blocked or misleading (many providers accept every address), and can get the probing IP blocklisted. This Actor stays at the DNS level on purpose and is priced accordingly.

**Why is jane@gmial.com marked disposable?**
Some typo domains of big providers are registered by third parties and collect mail, so the open source disposable list includes them. The `possible_typo` reason and `suggestion` tell you what was probably meant.

**Can I use my own DNS servers?**
Yes, set `dnsServers`, or switch `dnsMode` to `doh` for DNS over HTTPS.

**Does it find or collect email addresses?**
No. It only checks the addresses you give it. It does not scrape, search for or store addresses anywhere other than your own run's dataset.

### Data sources and licenses

- Disposable domains: [disposable-email-domains](https://github.com/disposable-email-domains/disposable-email-domains), released under **CC0 1.0** (public domain dedication), snapshot of 2026-10-27. Some extra checks for subdomains are added by this Actor.
- Role account, free provider, typo and mail provider rules are maintained in this Actor.

### Legal and privacy

Email addresses are personal data in many jurisdictions (GDPR, CCPA and others). Only submit addresses you are allowed to process, for example your own customers or contacts with a lawful basis. The Actor processes them only inside your run, sends only domain names to DNS resolvers, and keeps results in your own Apify storage, which you control and can delete.

### Support

Questions, bugs or feature requests: open an issue on the Actor's Issues tab, or email joesimo@mail.grokbot.com.

More examples and the docs for all 6 of our Actors: [github.com/Joe-Simo/apify-actors](https://github.com/Joe-Simo/apify-actors/tree/main/email-validator). Store page: https://apify.com/joesimo/email-validator

# Actor input Schema

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

One address per line. Wrappers such as <code>Jane Doe \<jane@acme.com></code> or <code>mailto:</code> are cleaned automatically. Up to a few hundred thousand per run. Only addresses you supply are checked; this Actor never collects or finds emails. If you start a run with no emails, no bulk paste and no dataset (for example an API call with <code>{}</code>), it checks the 6 sample addresses shown here as a demo (about $0.006).

## `emailsText` (type: `string`):

Paste a list (for example a CSV column). Addresses can be separated by new lines, commas, semicolons or tabs. Tokens without an @ (such as a header row) are skipped and not charged.

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

Optional. Check the emails stored in one of your Apify datasets, for example the output of another Actor run. Combined with the lists above.

## `datasetField` (type: `string`):

Field that holds the email in each dataset item. Dot paths work (<code>contact.email</code>). Arrays of emails are expanded.

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

Check at most this many unique addresses. 0 = all. You pay per address checked.

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

Check each address once (case-insensitive domain and local part). Duplicates are not saved and not charged.

## `roleAsRisky` (type: `boolean`):

Role or shared mailboxes such as <code>info@</code>, <code>sales@</code> or <code>support@</code> get status risky. Turn off to only flag them with <code>isRoleAccount</code>.

## `disposableStatus` (type: `string`):

Status for throwaway domains such as mailinator.com. They can receive mail, so the default is risky.

## `freeAsRisky` (type: `boolean`):

For B2B lists: give gmail.com, yahoo.com, outlook.com and other free consumer providers status risky. Off by default; <code>isFreeProvider</code> is always filled.

## `checkMxHosts` (type: `boolean`):

Also resolve the host names in the MX records. Catches domains whose MX points to a host that does not exist or to localhost.

## `checkSpfDmarc` (type: `boolean`):

Report whether the domain publishes SPF and DMARC records and the DMARC policy. Informational, it does not change the status.

## `includeSpfRecord` (type: `boolean`):

Add the raw SPF TXT record to each result as <code>spfRecord</code>.

## `dnsMode` (type: `string`):

Plain DNS queries the servers below. If they fail the startup check, the Actor falls back to the system resolver and then to DNS over HTTPS (Cloudflare and Google) automatically.

## `dnsServers` (type: `array`):

Resolver IP addresses, used in rotation. Leave empty to use the system resolver.

## `dohFallback` (type: `boolean`):

When a plain DNS lookup still times out or fails after all retries, ask Google DNS over HTTPS once before giving up. Turns many unknown results into real answers. Ignored when DNS transport is already DNS over HTTPS.

## `dnsConcurrency` (type: `integer`):

Maximum DNS queries in flight at once.

## `dnsTimeoutSecs` (type: `integer`):

Timeout per DNS query attempt.

## `dnsRetries` (type: `integer`):

Retries for timeouts and server failures, rotating resolvers with backoff. Lookups that still fail give status unknown.

## Actor input object example

```json
{
  "emails": [
    "jane@acme.com",
    "sales@acme.com"
  ],
  "datasetField": "email",
  "maxItems": 0,
  "dedupe": true,
  "roleAsRisky": true,
  "disposableStatus": "risky",
  "freeAsRisky": false,
  "checkMxHosts": true,
  "checkSpfDmarc": true,
  "includeSpfRecord": false,
  "dnsMode": "udp",
  "dnsServers": [
    "1.1.1.1",
    "8.8.8.8"
  ],
  "dohFallback": true,
  "dnsConcurrency": 50,
  "dnsTimeoutSecs": 4,
  "dnsRetries": 2
}
```

# Actor output Schema

## `results` (type: `string`):

One item per address: status, reasons, MX, disposable, role and free provider flags, SPF and DMARC.

## `runSummary` (type: `string`):

Counts by status and reason, duplicates removed and DNS stats.

# 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": [
        "john.doe@gmail.com",
        "info@apify.com",
        "someone@mailinator.com",
        "jane@gmial.com",
        "bad.address@",
        "user@example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("joesimo/email-validator").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": [
        "john.doe@gmail.com",
        "info@apify.com",
        "someone@mailinator.com",
        "jane@gmial.com",
        "bad.address@",
        "user@example.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("joesimo/email-validator").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": [
    "john.doe@gmail.com",
    "info@apify.com",
    "someone@mailinator.com",
    "jane@gmial.com",
    "bad.address@",
    "user@example.com"
  ]
}' |
apify call joesimo/email-validator --silent --output-dataset

```

## MCP server setup

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

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/IjzqMb3ekByrUpGLO/builds/jRgMSCPxS6CRAgxMr/openapi.json
