# Email Validator & List Cleaner (`enisbodlli/email-validator`) Actor

Sorts an email list into valid, invalid, risky and unknown addresses, with the reason for each, for marketers, sales teams and developers. Checks syntax, the domain's mail server, disposable and role addresses, and typos. It checks the address and its domain, not the individual mailbox.

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

## Pricing

from $0.40 / 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 Validator & List Cleaner

Clean an email list before you send to it. This **email validator** checks every address you give
it for correct **syntax**, looks up whether its **domain is set up to receive mail**, flags
**disposable**, **role** and **free-provider** addresses, names the **mail provider** behind the
domain, and suggests a fix for **typos** such as `gmial.com`. Paste the list, upload a **CSV or TXT
file**, or point it at an Apify dataset. To try it, leave the five sample addresses in the input and
click **Start**: you get one result per address, as a table you can export.

It checks the address and its domain, not the individual mailbox. The Actor does not contact the
mail server to ask whether a particular inbox exists, so `valid` means "correctly formed, and the
domain is set up to receive mail". It does not mean "this person's inbox exists".

Nothing is scraped and no email is sent. You supply the addresses, and the only outside information
the Actor uses is the public DNS record of each domain.

### What you can do with this email list cleaner

- **Clean a mailing list before a campaign.** Remove malformed addresses and addresses on domains
  that no longer exist or take no mail. These bounce every time.
- **Catch typos.** `user@gmial.com` comes back with the suggestion `user@gmail.com`, so you can
  correct the address and keep the contact.
- **Filter out throwaway sign-ups.** Addresses from disposable email services are flagged.
- **Sort a lead list.** Tell shared mailboxes such as `info@` or `sales@` from personal ones, and
  free-provider addresses from company addresses.
- **See which mail service a company uses.** `mailProvider` tells you whether a domain's mail goes
  to Google, Microsoft, Zoho, Proofpoint or another known service.
- **Clean the output of another Actor.** Give the dataset of a contact scraper as input and get its
  addresses checked.
- **Validate emails from your own code or an AI agent.** Start the Actor through the Apify API and
  read the results as JSON.

### Is a list cleaner enough, or do you need a mailbox verifier?

A list cleaner is enough when the question is "which of these addresses are certainly bad?". This
Actor finds:

- addresses that are not correctly formed,
- domains that do not exist,
- domains that take no mail,
- throwaway addresses from disposable email services,
- mistyped domains of well-known mailbox providers.

These answers come from the address itself and from public DNS records, so they are dependable. For
a list of your own subscribers or customers, collected through a sign-up form, this removes the
failures you can know about in advance.

You need a mailbox verifier when the question is "does this exact inbox exist?". That is the case
for an old list where people have since left their companies, for a list of guessed addresses such
as `first.last@company.com`, or when your sending service demands a bounce rate close to zero. A
mailbox verifier asks the mail server of each domain about each address. This Actor does not, so an
address like `nobody.by.this.name@gmail.com` is `valid` here: it is correctly formed and
`gmail.com` receives mail. For the same reason there is no numeric score: without a mailbox check a
score would be an invented number.

### How to validate email addresses in bulk

1. Give the list in one of three ways, or in several at once:
   - paste the addresses under **Email addresses**, one per line;
   - upload a **CSV or TXT file**, or paste a link to one;
   - pick an Apify **Dataset** that holds the addresses.
2. Click **Start**. Results are saved while the run is going, so a run you stop keeps what it
   finished.
3. Open the **Output** tab and export the table as JSON, CSV or Excel, or read it through the API.
4. Remove the `invalid` addresses, decide what to do with the `risky` ones, and run the `unknown`
   ones again later. Then look at `didYouMean` for addresses you can correct.

### Pricing

You pay per email that gets an answer, plus $0.00005 per run start. Addresses the check could not
answer are returned free. Platform usage is included, so there is nothing else to pay.

| Apify plan | Price per 1,000 emails | Per email |
|---|---|---|
| Free and Bronze | $0.50 | $0.0005 |
| Silver | $0.45 | $0.00045 |
| Gold | $0.40 | $0.0004 |

- 10,000 addresses cost at most $5.00 on the Free and Bronze plans and $4.00 on Gold.
- 500 addresses cost at most $0.25.

Repeated addresses are removed before the check, so you pay for each address once. An address is
charged when the check answered it: `valid`, `invalid` or `risky`. An address it could not answer
(`unknown`: the DNS lookup failed on all three attempts, or the check itself stopped with an error)
is still in the results, so you can run it again later, and costs nothing.

You can set a maximum charge per run. The Actor then checks only as many addresses as that limit
covers, from the top of the list, and reports how many it left out. `unknown` addresses do not
count against the limit.

You pay only for results that are stored. A run that is aborted, fails or times out keeps the
results it stored and charges those and nothing else. When Apify moves a run to another server, or
you resurrect a run, it continues after the results that are already stored instead of starting
over. Starting a **new** run with the same list checks and charges the whole list again.

### Input

| Field | What it does |
|---|---|
| Email addresses (`emails`) | The addresses to check, as a list. Up to 200,000 per run. |
| CSV or TXT file (`fileUrl`) | A file with the addresses: upload it in the input form, or give a link that opens without a login. Up to 50 MB. |
| Dataset (`datasetId`) | An Apify dataset that holds the addresses. The Actor only reads it. |
| Column or field with the addresses (`emailColumn`) | The CSV column or dataset field to read. Only needed when it is not called `email`. |

At least one of the first three is needed. Addresses from all of them are checked together. Blank
entries and repeats are removed; upper and lower case count as the same address, and the first
spelling is kept. One run takes up to 200,000 different addresses; split a longer list into
several runs.

**CSV and TXT files.** A TXT file has one address per line. In a CSV file the separator may be a
comma, a semicolon or a tab, and cells may be quoted. The Actor reads one column: the one named in
`emailColumn`; otherwise the one whose header is `email` (or contains "email" or "e-mail");
otherwise the first column that holds an address. A first line without an `@` is taken as a header
and is not checked. Excel workbooks (`.xlsx`) are not read; save the sheet as CSV first.

**Datasets.** Addresses are read from the field `email`, or from the field named in `emailColumn`.
A field that holds a list of addresses gives one result per address. Rows without the field are
skipped.

Example with a pasted list:

```json
{
    "emails": ["info@apify.com", "support@github.com", "user@gmial.com", "someone@mailinator.com", "not-an-email"]
}
```

Example with a file whose addresses are in the column "Work email":

```json
{
    "fileUrl": "https://example.com/leads.csv",
    "emailColumn": "Work email"
}
```

### Output

One item per address, in the order of your list. Below are three results from a run of the sample
input: a valid address, an invalid one and a risky one.

```json
[
    {
        "email": "info@apify.com",
        "normalized": "info@apify.com",
        "status": "valid",
        "reason": "mx_found",
        "isValidSyntax": true,
        "domain": "apify.com",
        "hasMx": true,
        "mxHost": "aspmx.l.google.com",
        "mailProvider": "Google",
        "isDisposable": false,
        "isRoleAddress": true,
        "isFreeProvider": false,
        "didYouMean": null,
        "checkedAt": "2026-10-04T14:43:25.249Z"
    },
    {
        "email": "not-an-email",
        "normalized": null,
        "status": "invalid",
        "reason": "invalid_syntax",
        "isValidSyntax": false,
        "domain": null,
        "hasMx": null,
        "mxHost": null,
        "mailProvider": null,
        "isDisposable": null,
        "isRoleAddress": null,
        "isFreeProvider": null,
        "didYouMean": null,
        "checkedAt": "2026-10-04T14:43:25.234Z"
    },
    {
        "email": "someone@mailinator.com",
        "normalized": "someone@mailinator.com",
        "status": "risky",
        "reason": "disposable",
        "isValidSyntax": true,
        "domain": "mailinator.com",
        "hasMx": true,
        "mxHost": "mail2.mailinator.com",
        "mailProvider": null,
        "isDisposable": true,
        "isRoleAddress": false,
        "isFreeProvider": false,
        "didYouMean": null,
        "checkedAt": "2026-10-04T14:43:25.254Z"
    }
]
```

In the same run, `user@gmial.com` comes back with `"didYouMean": "user@gmail.com"`, and
`support@github.com` with `"mailProvider": "Microsoft"`.

| Field | Meaning |
|---|---|
| `email` | The address as you entered it, with surrounding spaces removed. An entry longer than 320 characters is cut there. |
| `normalized` | The address with its domain in lower case. `null` when the syntax is invalid. |
| `status` | `valid`, `invalid`, `risky` or `unknown`. |
| `reason` | Short code for why the address got its status. |
| `isValidSyntax` | Whether the address is correctly formed. |
| `domain` | The domain of the address, in lower case. |
| `hasMx` | `true` when the domain has an MX record, the DNS entry that names its mail server, and that server exists. `false` when it has none. |
| `mxHost` | The server that takes mail for the domain. `null` when there is none. |
| `mailProvider` | The mail service that server belongs to, such as `Google` or `Microsoft`. `null` when it is not one the Actor knows. |
| `isDisposable` | The domain belongs to a disposable (temporary) email service. |
| `isRoleAddress` | A shared address such as `info@`, `support@` or `sales@`, not a person. |
| `isFreeProvider` | The domain is a free mailbox provider such as `gmail.com` or `yahoo.com`. |
| `didYouMean` | A corrected address when the domain looks like a typo of a well-known provider. |
| `checkedAt` | When the address was checked, in UTC. |

A field is `null` when it could not be determined, for example every domain field of an address
with invalid syntax. The role, free-provider, mail-provider and typo fields are there for you to
filter on; they do not change the status.

Each run also stores a summary record, `RUN_SUMMARY`, in its key-value store: how many addresses
the list had, how many were checked and not checked, the count per status, and whether the run was
complete, ended by the charge limit, or aborted.

#### Mail providers

`mailProvider` is read from the MX host, never guessed from the domain name. The Actor names these
services: Google (Gmail and Google Workspace), Microsoft (Outlook.com and Microsoft 365), Yahoo
(with AOL), iCloud, Zoho, Proton, Fastmail, Amazon (WorkMail or SES), GMX, Tuta, Yandex, Mail.ru,
Tencent QQ, NetEase, Rackspace, GoDaddy, IONOS, Namecheap, OVHcloud, Titan and Migadu, and the mail
security gateways Proofpoint, Mimecast, Barracuda and Cloudflare. When a domain's mail passes
through a gateway, the gateway is what is named; the mailbox service behind it is not visible from
outside. Every other mail host gives `null`.

### What each email validation status and reason means

| Status | Meaning | What to do |
|---|---|---|
| `valid` | The address is correctly formed, its domain is set up to receive mail, and the domain is not a known disposable service. The mailbox itself is not checked. | Keep it. |
| `invalid` | The address cannot receive mail: it is malformed, or its domain does not exist or takes no mail. | Remove it. |
| `risky` | The domain belongs to a disposable email service. | Usually remove it. Your decision. |
| `unknown` | The check gave no answer at the time of the run. | Run it again later. |

| Reason | Status | Meaning |
|---|---|---|
| `mx_found` | valid | The domain is set up to receive mail. |
| `invalid_syntax` | invalid | Not a correctly formed email address. |
| `domain_not_found` | invalid | The domain does not exist. |
| `no_mail_server` | invalid | The domain exists, but nothing takes mail for it: it declares that it takes none, it has neither a mail record nor an address, or its mail record points to a server that does not exist. |
| `disposable` | risky | The domain belongs to a disposable email service, whose addresses are made to be thrown away. |
| `dns_error` | unknown | The DNS lookup for the domain failed or timed out on each of three attempts, so nothing could be established. |
| `check_failed` | unknown | The check stopped with an internal error. This should not happen; please report it. |

### Limits

- The individual mailbox is not checked. A `valid` address can still bounce, because the mailbox was
  closed or the name before the `@` is misspelled.
- A result describes the moment of the check. Domains expire and change.
- A domain without an MX record still counts as set up for mail when it has an address record,
  because that is where mail for it is sent. Many such domains are only websites. They come back
  `valid` with `hasMx: false`. For a stricter list, keep only addresses with `hasMx: true`.
- The disposable-domain check uses a list of about 9,000 known domains that ships with the Actor. A
  service that appeared last week may be missing.
- Typo suggestions cover well-known mailbox providers only. A suggestion does not change the status,
  so look at `didYouMean` as well as `status`.
- International domain names are supported and are shown in their ASCII form (`xn--...`) in
  `domain` and `normalized`. The part before the `@` must be plain, unquoted ASCII; addresses with
  accented or non-Latin characters there are reported as `invalid_syntax`.
- One run takes up to 200,000 different addresses, and a file of up to 50 MB.
- A list of 10,000 addresses on 9,200 different domains took about one minute in a test. A list
  with many domains whose name servers no longer answer takes longer, about 10 seconds for each
  such domain, shared between 50 lookups at a time.

### FAQ

#### Can it check if an email address exists?

Not the individual mailbox. It checks that the address is correctly formed and that its domain
exists and is set up to receive mail. Whether a mailbox with that name exists on the domain is
something only the mail server knows, and this Actor does not ask it. See "Is a list cleaner
enough, or do you need a mailbox verifier?" above.

#### Does this email checker send any emails?

No. It sends nothing and does not connect to any mail server. It reads the address and looks up the
domain, and the mail host the domain names, in DNS. Nobody on your list is contacted.

#### Can I upload a CSV file?

Yes. Upload it under **CSV or TXT file**, or paste a link to it. The addresses must be in one
column; the Actor finds the column named `email`, or you name the column in `emailColumn`. The
other columns are ignored and do not appear in the output, so match the results to your file on
the `email` field.

#### What happens to my email list?

It is processed inside your own run on the Apify platform, and the results go only to the dataset of
that run, in your account. When the list comes from a file or a dataset, the run also keeps a
working copy of it in its own key-value store, so that a restarted run checks the same list; it is
stored with the run and deleted with it. The Actor passes the list to no other service. The only
requests it makes to the outside are DNS lookups for each domain, which do not include the part
before the `@`, and the download of the file you linked. The developer of this Actor cannot see
your input or your results. The one exception is yours to control: the Apify account setting *Share
run data with developers*, which is off unless you turn it on.

#### How accurate is email validation?

What the Actor reports is dependable, because it comes from the address itself and from the public
DNS record of its domain: a malformed address is malformed, and a domain that takes no mail cannot
receive yours. What it cannot tell you is whether the mailbox exists, so `valid` is not a promise
that a message will arrive. The disposable flag depends on a list of known services and can miss a
new one.

#### Why is a mistyped address "valid"?

A mistyped domain can be a real domain that someone else owns. The address is then correctly formed
and its domain can receive mail, and the status reports that. The typo is reported separately: when the
domain is one or two characters away from a well-known mailbox provider, `didYouMean` holds the
corrected address.

#### Why is an address "unknown"?

The DNS lookup for its domain failed or timed out three times in a row, so the Actor could not tell
whether the domain receives mail. This is usually temporary. Run those addresses again later;
`unknown` addresses are not charged.

#### What happens when a run is stopped or restarted?

The results that are stored stay, and you pay for those only. If you resurrect the run, or Apify
moves it to another server, it continues after the last stored result. The run's status message
shows the progress while it runs, and the final message says how many addresses were checked and
how many were left out.

#### Is a role address such as info@ invalid?

No, it can receive mail like any other address. `isRoleAddress` tells you that it is a shared
mailbox and not a person. Some senders leave shared mailboxes out of their lists, so the flag lets
you filter them. Whether to keep them is your decision.

#### Where do I report a problem?

Open an issue on the **Issues** tab of this Actor. Say which status and reason you got and what you
expected, and name the domain of the address. Issues are answered there.

### More Actors from this developer

Contacts and lists:

- [Website Contact Scraper](https://apify.com/enisbodlli/website-contact-scraper)

Jobs:

- [Company Jobs Search](https://apify.com/enisbodlli/company-jobs-search)
- [ATS Job Postings: Workday, Greenhouse, Lever & Ashby](https://apify.com/enisbodlli/ats-job-postings)
- [Workday Jobs Scraper](https://apify.com/enisbodlli/workday-jobs-scraper)
- [Greenhouse Jobs Scraper](https://apify.com/enisbodlli/greenhouse-jobs-scraper)
- [Lever Jobs Scraper](https://apify.com/enisbodlli/lever-jobs-scraper)
- [Ashby Jobs Scraper](https://apify.com/enisbodlli/ashby-jobs-scraper)

Company registers:

- [Handelsregister Scraper: German Company Register](https://apify.com/enisbodlli/handelsregister-scraper)
- [North Data Scraper: German & European Companies](https://apify.com/enisbodlli/northdata-company-scraper)
- [European Company Registry Search](https://apify.com/enisbodlli/eu-company-registry-search)
- [US Business Entity Search & New Business Filings](https://apify.com/enisbodlli/us-business-registry-search)
- [Brazil CNPJ Scraper: Company Search & Lookup](https://apify.com/enisbodlli/brazil-cnpj-company-search)

# Actor input Schema

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

The email addresses to check, one per line. Blank lines and repeated addresses are removed, and every address that is left gives one result. Up to 200,000 addresses per run.

## `fileUrl` (type: `string`):

Upload a file with your addresses, or paste a link to one. A CSV file needs the addresses in one column; the column named "email" is used, or else the first column that holds an address. A TXT file has one address per line. The link must open without a login, and the file may be up to 50 MB.

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

An Apify dataset that holds the addresses, for example the output of a scraper. The Actor only reads it. Addresses are taken from the field "email" unless another one is named below.

## `emailColumn` (type: `string`):

Only needed when the addresses are not in a column or field called "email": the name of the CSV column (its header) or of the dataset field that holds them, for example "contactEmail".

## Actor input object example

```json
{
  "emails": [
    "info@apify.com",
    "support@github.com",
    "user@gmial.com",
    "someone@mailinator.com",
    "not-an-email"
  ],
  "fileUrl": "https://example.com/newsletter-list.csv",
  "datasetId": "aBcDeFgHiJkLmNoPq",
  "emailColumn": "email"
}
```

# Actor output Schema

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

One item per email address, in the order of the input list: the address, its status (valid, invalid, risky or unknown), the reason, and the domain facts behind it (MX host, mail provider, disposable, role address, free provider, typo suggestion).

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

One JSON record with the totals of the run: addresses in the list, checked, not checked, and how many were valid, invalid, risky and unknown, plus whether the run was complete or was ended by the charge limit or an abort.

# 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",
        "support@github.com",
        "user@gmial.com",
        "someone@mailinator.com",
        "not-an-email"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("enisbodlli/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": [
        "info@apify.com",
        "support@github.com",
        "user@gmial.com",
        "someone@mailinator.com",
        "not-an-email",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("enisbodlli/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": [
    "info@apify.com",
    "support@github.com",
    "user@gmial.com",
    "someone@mailinator.com",
    "not-an-email"
  ]
}' |
apify call enisbodlli/email-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,enisbodlli/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/UXgBi8hTHU1dceG4Y/builds/Ja9Wj2nYaMNpKLft4/openapi.json
