# Email List Cleaner: Bulk Email Validator, MX & Disposable Check (`solidfetch/email-list-checker`) Actor

Clean an email list in bulk without sending anything. Checks syntax, MX records, disposable domains, free-mail and role addresses (info@, sales@), and suggests fixes for typos like gmial.com. Each address gets a valid, risky or invalid verdict with reasons. No SMTP probing, so no catch-all claims.

- **URL**: https://apify.com/solidfetch/email-list-checker.md
- **Developed by:** [Solid Fetch](https://apify.com/solidfetch) (community)
- **Categories:** Lead generation
- **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, MX and Disposable Check

Clean an e-mail list in bulk before you send to it. 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 reasons. Paste a column from a spreadsheet or pass a list. A domain-only mode checks a list of domains.

**This actor does not probe mail servers.** It never connects to an SMTP server, never sends a message and never asks a server whether a mailbox exists. It uses DNS only. That is why it is fast and cheap, and it is also why it cannot tell you that `jane@company.com` is a real mailbox, and why 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". 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,
    "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`. |
| `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.

# 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@gmial.com",
    "test@mailinator.com"
  ],
  "mode": "emails"
}
```

# Actor output Schema

## `results` (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",
        "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/T25TyVMou4NhMjipJ/openapi.json
