# Email List Cleaner: Bulk Email Validator, $0.80/1k, MX Check (`solidfetch/email-list-checker`) Actor

Email validation: validate an email list in bulk, remove bad, disposable and role addresses (info@), fix typos like gmial.com, check syntax and MX. Verifier and list cleaner that cuts bounces from dead domains. Valid, risky or invalid with a reason. $0.80/1k. Domain checked, mailbox not: no SMTP.

- **URL**: https://apify.com/solidfetch/email-list-checker.md
- **Developed by:** [Solid Fetch](https://apify.com/solidfetch) (community)
- **Categories:** Agents, Lead generation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 email checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 List Cleaner: Bulk Email Validator, $0.80/1k, MX Check

Clean an email list before you send to it: validate an email list in bulk, remove bad and disposable addresses and catch the typos and dead domains that cause hard bounces. An email list cleaner, email validator and email verifier for marketers and sales teams, with the verification done at domain level: syntax, MX records, disposable and role addresses. Paste a column of addresses and each one gets valid, risky or invalid with a reason (valid means the domain takes mail, not that the mailbox exists), a suggested fix for typos (gmial.com becomes gmail.com) and a flag for role addresses (info@). It checks syntax and MX records only, with no SMTP probing, so it cannot catch every bounce: a mailbox that does not exist on a working domain still comes back valid. $0.80 per 1,000 checked addresses on the free plan; bad syntax and repeats are free.

```json
{ "emails": ["info@example.com", "jane.doe@gmial.com", "test@mailinator.com"] }
```

One row of the result (shortened, an invented domain; see [Output](#output) for all fields):

```json
{ "email": "jane.doe@acme.example", "domain": "acme.example", "verdict": "valid", "reason": "mx-found", "explanation": "The domain has MX records", "syntaxValid": true, "mxFound": true, "mxHost": "mx1.acme.example" }
```

Clean an email list in bulk before you send to it, for **$0.80 per 1,000 checked addresses** on the free plan ($0.50 per 1,000 on Gold and above). Bad syntax, repeats and failed lookups are free. For every address the actor checks the syntax, looks up the domain's MX records in DNS, flags disposable domains, free-mail providers and role addresses (info@, sales@), suggests a fix for common typos (gmial.com becomes gmail.com) and gives a final verdict, `valid`, `risky` or `invalid`, with the reason in a code and in a plain sentence. Paste a column from a spreadsheet or pass a list. There is no file upload, so copy the column and paste it into the text field. A domain-only mode checks a list of domains.

**No SMTP probing, said plainly.** The actor never connects to an SMTP server, never sends a message and never asks a server whether a mailbox exists. It uses DNS only, so it cannot tell you that `jane@company.com` is a real mailbox, and it never claims a domain is catch-all. A `valid` verdict means "the address is well formed and its domain can receive mail", not "the mailbox exists": `nobody-here@company.com` comes back `valid` when `company.com` takes mail, so every row has `mailboxVerified: false`.

Why that is often the better trade for a list clean-up:

- **Speed.** One DNS lookup per domain, however many addresses share it. No waiting on slow or greylisting mail servers.
- **Nothing to get blocked or blacklisted.** No mail server on your list is contacted, so no server sees a probe, rate-limits it or puts an IP on a blocklist over it.
- **A clear reason for every verdict.** Each row says why: `domain-not-found`, `no-mail-records`, `disposable-domain`, `role-account`, `possible-typo` and so on, so you decide what to drop and what to keep. SMTP checks can end in "unknown" for servers that accept every address or refuse to answer.

It removes the structural junk: dead domains, domains that take no mail, throwaway services, shared role inboxes and typos. If you need a mailbox-level answer, you need an SMTP-probing service, and those come with their own error rates and blocked IPs.

### What people use it for

- **Cleaning a list before a campaign.** Drop addresses that cannot bounce for structural reasons: typos in the syntax, domains that do not exist, domains that publish no mail records. This protects your sender reputation from the cheapest kind of hard bounce.
- **Removing throwaway sign-ups.** The disposable flag uses the maintained open list `disposable-email-domains` (about 9,900 domains).
- **Splitting people from teams.** The role flag marks info@, sales@, admin@, support@ and similar shared addresses, and the free-provider flag marks gmail, outlook, yahoo and other public mailboxes, so you can route a lead list by company or by person.
- **Catching typos while they can still be fixed.** `jane@gmial.com` comes back with `suggestion: "jane@gmail.com"`.
- **Cleaning a domain list.** Domain mode takes domains, URLs or addresses and returns the MX, disposable, free-mail and typo checks for each domain.

### How to use it

A list:

```json
{
    "emails": ["info@example.com", "jane.doe@gmial.com", "test@mailinator.com"]
}
```

A pasted column (one per line, or separated by commas, semicolons or tabs):

```json
{
    "text": "jane@acme.com\nbob@acme.com, sue@globex.com\nNot an address"
}
```

Domains only:

```json
{
    "mode": "domains",
    "text": "gmail.com\nhttps://www.mailinator.com/\nsome-typo-gmial.com"
}
```

Both fields can be used in one run. `Name <a@b.com>` and `mailto:` links are cleaned, and a field name the actor does not know stops the run with a hint such as `"email" (did you mean "emails"?)`, so a typo cannot give you a run that ignored a setting.

### Input

| Field | What it does |
|---|---|
| `emails` | List of addresses. An entry may hold several addresses separated by commas, semicolons or line breaks. Up to 100,000 entries per run. |
| `text` | A text blob with one address per line, or separated by commas, semicolons or tabs. Used together with `emails`. |
| `mode` | `emails` (default) or `domains`. In domain mode, URLs and addresses are reduced to their domain. |

Repeats are found after normalising (lower case, domain in punycode, a trailing dot dropped). `Jane@Example.com` and `jane@example.com` are the same address. `jane+news@example.com` is a different one, because the tag may route to a different folder or a different person.

### Output

One row per input, in input order, so you can put the result next to your original column. The row is flat, so the CSV export works as is. An example of the row shape (invented domain):

```json
{
    "input": " Jane.Doe@Acme.example ",
    "email": "jane.doe@acme.example",
    "domain": "acme.example",
    "verdict": "valid",
    "reason": "mx-found",
    "reasons": ["mx-found"],
    "explanation": "The domain has MX records",
    "syntaxValid": true,
    "mxFound": true,
    "mxCount": 2,
    "mxHost": "mx1.acme.example",
    "mxRecords": [
        { "exchange": "mx1.acme.example", "priority": 10 },
        { "exchange": "mx2.acme.example", "priority": 20 }
    ],
    "dnsStatus": "mx",
    "hasARecord": null,
    "hasAaaaRecord": null,
    "disposable": false,
    "freeProvider": false,
    "roleAccount": false,
    "role": null,
    "suggestion": null,
    "smtpChecked": false,
    "mailboxVerified": false,
    "duplicateOf": null,
    "checkedAt": "2026-10-22T12:00:00.000Z"
}
```

| Field | Meaning |
|---|---|
| `input` | The entry as you gave it. |
| `email`, `domain` | The normalised address (lower case, domain in ASCII) and its domain. `null` when the syntax is invalid. |
| `verdict` | `valid`, `risky` or `invalid`. See the rules below. |
| `reason`, `reasons`, `explanation` | The main reason code, all reason codes (heaviest first) and the same reasons as plain sentences. |
| `syntaxValid` | False when the address breaks a syntax rule. The reason code says which one. |
| `mxFound`, `mxCount`, `mxHost`, `mxRecords` | The MX records, sorted by priority. `mxFound` is `null` when the lookup failed. |
| `dnsStatus` | `mx`, `a-fallback`, `null-mx`, `no-mail-records`, `nxdomain` or `error`. |
| `hasARecord`, `hasAaaaRecord` | Only filled when the domain has no MX: whether it has an address record that mail would fall back to. |
| `disposable`, `freeProvider` | Flags for the domain. |
| `roleAccount`, `role` | A shared function address and which one (`info`, `sales`, `noreply`). Not in domain mode. |
| `suggestion` | The corrected address (or domain in domain mode) when the domain looks like a mistyped popular one. |
| `smtpChecked` | Always `false`. |
| `mailboxVerified` | Always `false`: a `valid` row says the domain takes mail, not that this mailbox exists. |
| `duplicateOf` | The normalised address this row repeats, so repeats can be filtered out. |
| `checkedAt` | When the DNS was asked. |

The run also writes a `SUMMARY` record with the counts by verdict and by reason, how many rows were charged, and how many DNS lookups were made.

### Verdict rules

The verdict is `invalid` if any invalid reason applies, otherwise `risky` if any risky reason applies, otherwise `valid`.

| Reason | Verdict | When |
|---|---|---|
| Syntax codes such as `missing-at`, `local-part-too-long`, `domain-without-dot`, `invalid-domain-character` | invalid | The address breaks a syntax rule. The code names the first rule it breaks. |
| `domain-not-found` | invalid | DNS says the domain does not exist (NXDOMAIN). |
| `domain-rejects-mail` | invalid | The domain publishes a null MX record (RFC 7505), which says it accepts no mail. |
| `no-mail-records` | invalid | The domain exists but has no MX and no A or AAAA record. |
| `no-mx-address-fallback` | risky | No MX, but an A or AAAA record. Mail systems then try that address, but few real mailboxes work that way. |
| `dns-lookup-failed` | risky | Timeout or server failure. Nothing is known about the domain. Run it again. |
| `disposable-domain` | risky | The domain is a throwaway mailbox service. |
| `role-account` | risky | info@, sales@, admin@, support@, noreply@ and similar. |
| `possible-typo` | risky | The domain looks like a mistyped popular domain. A typo domain that does not exist is invalid, with the suggestion still given. |
| `international-local-part` | risky | Non-ASCII letters before the @, which many mail systems cannot deliver to. |
| `mx-found` | information | The domain has MX records. It is the only reason of a `valid` row. |

A free provider (gmail.com and the like) is not a reason by itself. It sets `freeProvider` and nothing else.

Syntax follows what mail systems accept in practice (RFC 5321 and 5322 without the exotic parts). Quoted local parts (`"john doe"@example.com`), comments and IP literals (`user@[1.2.3.4]`) are valid in the RFCs and rejected here, because no ordinary mailbox uses them.

### Pricing

Pay per event, prices on the Pricing tab.

| Event | Charged for |
|---|---|
| `email-checked` | Each address (or domain, in domain mode) that has valid syntax, is not a repeat of an earlier one, and got an answer from DNS. |

Free: inputs with invalid syntax, repeats of an earlier address, and rows where the DNS lookup failed (timeout or server failure), because the actor could not answer them. Rows past your maximum cost per run are not saved.

The final status line adds it up, for example `1,000 of 1,000 inputs checked: 790 valid, 150 risky, 60 invalid. 940 charged as email-checked, 60 free (35 with bad syntax, 25 repeats).`

### Limits

- **No mailbox check, no catch-all detection.** See the top of this page. `valid` says the domain can receive mail, not that `jane` exists there.
- **DNS only.** Each domain is looked up once per run, however many addresses share it. Lookups have a timeout of 5 seconds and one retry. A domain whose name servers do not answer ends as `dns-lookup-failed` and is free, and a re-run usually answers it.
- **Disposable list.** It is the open list `disposable-email-domains` (CC0 1.0, fetched 2026-10-22, 9,919 domains). New throwaway services appear every week, so the flag catches the known ones. A subdomain of a listed domain counts as listed.
- **Typo suggestions are for popular providers only** (gmail, yahoo, hotmail, outlook, icloud, protonmail, yandex). A company domain with a typo is not suggested, it just fails the MX check if the domain does not exist.
- **Role addresses are a list of common names**, not a guess. `sales.manager@` is not flagged, `sales@` is.
- **Up to 100,000 entries per run.**

### Privacy

Your addresses are your data. The actor does not log them (the log holds counts only, and an error message that quotes an address is masked to `j***@example.com`), does not keep them anywhere except the dataset of the run, and does not send them to any service other than the DNS resolver asked about the domain part. The run's restart record holds a hash of the list and counters, not addresses.

When a run fails, an error report with the error message (addresses masked) and the shape of the input (which fields are set, how long the lists are), never your input values or your results, is sent to the developer to fix bugs.

### Attribution

The disposable-domain flag uses the `disposable-email-domains` list (https://github.com/disposable-email-domains/disposable-email-domains), published under CC0 1.0 Universal. The actor is not affiliated with it.

Did this help? A review on this page, or an issue saying what is missing, helps other people find it.

# Changelog

This Actor's version history is a separate document: https://apify.com/solidfetch/email-list-checker/changelog.md

# Actor input Schema

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

One address per entry. An entry may also hold several addresses separated by commas, semicolons or line breaks. Repeats are dropped (the repeat gets a free row). Use this or the text box below, or both. Up to 100,000 entries per run.

## `text` (type: `string`):

Paste a column from a spreadsheet or a CSV export: one address per line, or separated by commas, semicolons or tabs. "Name <a@b.com>" and mailto: links are cleaned.

## `mode` (type: `string`):

Email addresses checks each address. Domains only takes domains (or URLs, or addresses, from which the domain is read) and returns the domain checks: MX records, disposable, free-mail provider and typo suggestion.

## Actor input object example

```json
{
  "emails": [
    "info@apify.com",
    "jane.doe@gmail.com"
  ],
  "text": "info@apify.com\nJane Doe <jane.doe@gmail.com>\nmailto:sales@example.com",
  "mode": "emails"
}
```

# Actor output Schema

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

Table with one row per input: verdict (valid, risky, invalid), main reason and all reasons, syntax, MX records, disposable, free-provider and role-address flags, typo suggestion. No SMTP check is made.

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

JSON record SUMMARY in the key-value store: counts by verdict and by reason.

# 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",
        "jane.doe@gmial.com",
        "test@mailinator.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("solidfetch/email-list-checker").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",
        "jane.doe@gmial.com",
        "test@mailinator.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("solidfetch/email-list-checker").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",
    "jane.doe@gmial.com",
    "test@mailinator.com"
  ]
}' |
apify call solidfetch/email-list-checker --silent --output-dataset

```

## MCP server setup

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

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/vejC7fzek4AaKVOJP/builds/GLJBJgVbbxJ06JBgP/openapi.json
