# Bulk Email Verifier: MX, Disposable, Role & Typo Check (`cliqtomedia/email-verifier`) Actor

Verify email lists without sending them to any third-party API: RFC syntax, DNS MX records, disposable, role and free-provider flags, typo suggestions and a deliverable / risky / undeliverable status with reasons. No SMTP mailbox check. No login, no API key.

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

## Pricing

Pay per event

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

Bulk Email Verifier checks a list of email addresses and gives every address a status: **deliverable**, **risky**, **undeliverable** or **unknown**, with the reasons. It checks syntax, DNS MX records, disposable and role addresses, free providers and domain typos. It does not open an SMTP connection, so it cannot confirm that a mailbox exists. No login and no API key. Your list is not sent to any third-party verification service.

Use it to clean a list before a campaign or a CRM import. The smallest run is one address; 1,000 addresses cost about $0.40.

![Bulk Email Verifier: paste an email list and get a status and reason for every address — syntax, MX and domain checks, disposable, role and typo flags, no third-party verification API](https://api.apify.com/v2/key-value-stores/XEZc0mrmlnDe2jFVI/records/README-BANNER-20261013)

### Table of contents

- [What it does](#what-it-does)
- [Quick start](#quick-start)
- [Use cases](#use-cases)
- [Input](#input)
- [Output](#output)
- [Pricing and limits](#pricing-and-limits)
- [API, export and integrations](#api-export-and-integrations)
- [Limitations](#limitations)
- [Troubleshooting](#troubleshooting)
- [FAQ](#faq)
- [Support](#support)

### What it does

For each address the Actor:

1. Checks the syntax by the email standards (RFC 5321 / 5322). For example, `john..doe@example.com` is invalid because of the two dots.
2. Looks up the domain in DNS: MX records, a "null MX" record ([RFC 7505](https://www.rfc-editor.org/rfc/rfc7505)) that says "this domain takes no mail", and the A/AAAA fallback when there is no MX.
3. Flags **disposable** domains (temporary inboxes like mailinator.com), **role** addresses (info@, support@, sales@…) and **free** providers (gmail.com, outlook.com…).
4. Suggests a fix for a likely typo: `hello@gmial.com` → `hello@gmail.com`.
5. Names the mail provider from the MX hosts, for example Google or Microsoft.
6. Gives one status and a list of reason codes.

All checks run inside the Actor. The list is not sent to an outside verification API. Only domain names go to DNS, like any mail server does.

#### Good fit

- Remove bad and risky addresses before an email campaign, so fewer emails bounce.
- Check emails found by a lead scraper before you add them to a CRM.
- Find typos and disposable addresses in sign-up lists.
- Check lists that you do not want to share with another verification company.

#### Not a fit / limits

- It does **not** connect to the mail server (SMTP). It cannot say if one mailbox exists, and it cannot detect catch-all domains. See [Limitations](#limitations).
- It does not send emails.
- It does not promise 100% accuracy. DNS and mail servers change all the time.

### Quick start

1. Open the **Input** tab.
2. Paste your addresses into **Email addresses** (`emails`), one per line. The form already has 5 example addresses.
3. Keep the other settings at their defaults.
4. Click **Start**.
5. Open the **Output** tab and check the **Status** and **Reasons** columns.

Expected result: one row per address. The 5 example addresses finish in a few seconds: 3 risky (role account, disposable domain) and 2 undeliverable (domain does not exist, invalid syntax).

### Use cases

| Buyer job | Input path | Output entity | Key fields | Next step |
| --- | --- | --- | --- | --- |
| Clean a list before a campaign | `emails` | One row per address | `status`, `reasons` | Keep `deliverable`, review `risky`, remove `undeliverable` |
| Check scraped leads | `inputDatasetId` + `emailField` | One row per address | `status`, `isRoleAccount`, `mxProvider` | Import good rows into a CRM |
| Fix sign-up typos | `emails` | One row per address | `didYouMean`, `reasons` | Ask the user to correct the address |
| Block temporary inboxes | `emails` | One row per address | `isDisposable` | Reject or flag the sign-up |

### Input

#### Input fields

| Field | Plain name | Type | Required | Default / prefill | Allowed values | Effect | Recommendation |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `emails` | Email addresses | array of strings | no\* | prefill: 5 example addresses | up to 10,000 in total | Addresses to check. A line with several addresses separated by commas, semicolons or spaces is split. | Paste one address per line. |
| `inputDatasetId` | Input dataset | string | no\* | empty | dataset picker in Console, dataset ID in the API | Adds addresses from an Apify dataset, e.g. the output of a lead scraper. Up to 20,000 dataset items are read. | Use it to chain this Actor after a scraper. |
| `emailField` | Email field in dataset | string | no | `email` | up to 100 characters | Which dataset field holds the address. Items without it are counted in the run summary. | Change it if your dataset uses another name, e.g. `contactEmail`. |
| `concurrency` | Parallel checks | integer | no | `20` | 5–50 | How many addresses are checked at the same time. | Use 50 for lists over 5,000 addresses. |
| `deduplicate` | Remove duplicates | boolean | no | `true` | true / false | Checks each address once (case-insensitive). | Turn it off only if you need one output row per input line. |

\* You need at least one address in `emails` or `inputDatasetId`. Both can be used together; the total limit is 10,000 addresses per run.

If there are no addresses, or more than 10,000, the run fails at once with a clear message and checks nothing.

#### Input examples

##### Check a pasted list

```json
{
  "emails": [
    "support@apify.com",
    "someone@nonexistentdomain-7c1e5b42.com",
    "test@mailinator.com",
    "sales@stripe.com",
    "john..doe@example.com"
  ]
}
```

Expected output: 5 rows. `support@apify.com` and `sales@stripe.com` are `risky` (role account), `test@mailinator.com` is `risky` (disposable), and the other two are `undeliverable`.

##### Check emails from a lead scraper dataset

```json
{
  "inputDatasetId": "YOUR_DATASET_ID",
  "emailField": "email",
  "concurrency": 50
}
```

Expected output: one row per address found in the dataset. The run summary shows how many dataset items had no address.

### Output

Each address gives one row in the dataset. The **Overview** view shows the main columns.

#### Output example

```json
{
  "email": "sales@stripe.com",
  "normalized": "sales@stripe.com",
  "status": "risky",
  "reasons": ["role_account", "mailbox_not_verified"],
  "syntaxValid": true,
  "domain": "stripe.com",
  "hasMx": true,
  "mxRecords": [{ "exchange": "aspmx.l.google.com", "priority": 10 }],
  "mxProvider": "Google",
  "isDisposable": false,
  "isRoleAccount": true,
  "isFreeProvider": false,
  "isCatchAll": null,
  "smtpCode": null,
  "smtpChecked": false,
  "didYouMean": null,
  "checkedAt": "2026-10-13T01:57:42.504Z"
}
```

This is one real row from the example input. `mxRecords` is shortened (the real row has 5 records).

#### Output fields

| Field | Meaning | Type | Can be empty? | Example |
| --- | --- | --- | --- | --- |
| `email` | The address as you gave it (trimmed) | string | no | `sales@stripe.com` |
| `normalized` | Lower-case address with an ASCII (punycode) domain | string | null if syntax is invalid | `sales@stripe.com` |
| `status` | Final result | string | no | `deliverable`, `risky`, `undeliverable`, `unknown` |
| `reasons` | Reason codes (see below) | array of strings | can be `[]` | `["role_account","mailbox_not_verified"]` |
| `syntaxValid` | Syntax is valid | boolean | no | `true` |
| `domain` | Domain in ASCII | string | null if there is no domain part | `stripe.com` |
| `hasMx` | Domain has MX records with a reachable mail host | boolean | null if DNS failed or syntax is invalid | `true` |
| `mxRecords` | MX records sorted by priority | array of `{exchange, priority}` | can be `[]` | `[{"exchange":"aspmx.l.google.com","priority":10}]` |
| `mxProvider` | Mail provider found from MX hosts | string | null if not recognised | `Google`, `Microsoft`, `Zoho` |
| `isDisposable` | Domain is a temporary inbox service | boolean | null if the domain cannot be read | `false` |
| `isRoleAccount` | Address is a role mailbox (info, support, sales…) | boolean | null if the local part cannot be read | `true` |
| `isFreeProvider` | Domain is free webmail | boolean | null if the domain cannot be read | `false` |
| `didYouMean` | Suggested fix for a domain typo | string | null if no typo is found | `hello@gmail.com` |
| `isCatchAll` | Always null: needs SMTP, which is not used | null | always null | `null` |
| `smtpCode` | Always null: no SMTP connection is made | null | always null | `null` |
| `smtpChecked` | Always false: the mailbox is not probed | boolean | no | `false` |
| `checkedAt` | Time of the check (ISO 8601) | string | no | `2026-10-13T01:57:42.504Z` |

#### Statuses

| Status | When | Charged? |
| --- | --- | --- |
| `deliverable` | Valid syntax, the domain accepts mail, no risk flags. The mailbox itself is not checked. | yes |
| `risky` | The domain accepts mail, but there is a flag: disposable, role account, possible typo, no MX (only A record), quoted or non-ASCII local part. | yes |
| `undeliverable` | Invalid syntax, the domain does not exist, null MX, no mail server, or MX hosts that do not resolve. | yes |
| `unknown` | DNS did not answer (timeout or server error). Try again later. | **no** |

#### Reason codes

- Syntax: `invalid_syntax` plus one detail, e.g. `invalid_dots_in_local_part`, `missing_at_or_part`, `multiple_at`, `contains_whitespace`, `invalid_domain`, `invalid_tld`, `domain_without_tld`, `local_part_too_long`, `address_too_long`; `quoted_local_part` and `non_ascii_local_part` make an address risky.
- Domain: `domain_not_found`, `null_mx_domain_accepts_no_mail`, `no_mail_server`, `mx_host_unresolvable`, `no_mx_a_record_fallback`, `dns_lookup_failed`.
- Flags: `disposable_domain`, `role_account`, `possible_domain_typo`.
- `mailbox_not_verified`: added to every deliverable and risky row, because the mailbox is not probed.

#### Run summary

The key-value store record `RUN_SUMMARY` has the run status (`COMPLETE`, `PARTIAL`, `INVALID_INPUT` or `ERROR`), a message, counts (input, unique, duplicates, processed, not processed, by status), charged events and the number of domains checked. It never contains email addresses.

### Pricing and limits

#### Pricing unit

This Actor uses pay-per-event pricing. You pay for:

| Event | When | Price |
| --- | --- | --- |
| Actor start | Once per run | $0.005 |
| Email checked | Once per address with status deliverable, risky or undeliverable | $0.40 per 1,000 (Free plan), lower on paid Apify plans: $0.38 Bronze, $0.36 Silver, $0.34 Gold per 1,000 |

- `unknown` rows are free.
- Duplicates removed by `deduplicate` are free.
- The start event is charged for every run, also when the input is wrong and the run fails.
- If the run stops early (time limit, your maximum charge, or an abort), you pay only for the rows saved in the dataset.

#### Cost examples (Free plan prices)

| Scenario | Addresses | Approximate charge |
| --- | ---: | ---: |
| Example input | 5 | $0.007 |
| Small list | 100 | $0.045 |
| Campaign list | 1,000 | $0.405 |
| Maximum per run | 10,000 | $4.005 |

#### Limits

- Up to 10,000 addresses per run. Split larger lists into several runs.
- Up to 20,000 items are read from an input dataset.
- In our test, 10,000 addresses on 5,660 domains took about 4.5 minutes with `concurrency` 50 and 256 MB memory. Many addresses on one domain are faster, because each domain is looked up once.
- Set **Maximum cost per run** in the run options to limit spending. The Actor stops before it goes over the limit and saves what it already checked.

### API, export and integrations

- **API:** start the Actor with the [Apify API](https://docs.apify.com/api/v2) and read the default dataset.
- **Export:** download results from the Output tab as JSON, CSV, Excel or other formats.
- **Chaining:** pass the dataset ID of a lead scraper run as `inputDatasetId`.
- **Scheduling and webhooks:** use Apify [schedules](https://docs.apify.com/platform/schedules) and [integrations](https://docs.apify.com/platform/integrations) to run it on new lists.

#### Use with AI agents (MCP)

- MCP server with only this Actor: `https://mcp.apify.com?tools=cliqtomedia/email-verifier`
- Smallest useful input: `{"emails": ["support@apify.com", "test@mailinator.com"]}`
- Key output fields: `status` (deliverable / risky / undeliverable / unknown), `reasons` (why), `didYouMean` (typo fix), `isDisposable`, `isRoleAccount`.
- A run with a few addresses takes a few seconds and costs about $0.006. Limit: 10,000 addresses per run.

### Limitations

- **No SMTP check.** Apify does not allow outgoing connections on port 25, so the Actor does not talk to mail servers. A wrong mailbox on a real domain (for example a made-up name at gmail.com) is shown as `deliverable` with `mailbox_not_verified`. Use this Actor as a fast, private first filter. If you need mailbox-level checks, use an SMTP verification service after it.
- **No catch-all detection.** `isCatchAll` is always null.
- **Lists change.** The disposable and free-provider lists come from open-source projects ([disposable-email-domains](https://github.com/disposable-email-domains/disposable-email-domains), MIT; [freemail](https://github.com/willwhite/freemail), ISC). New temporary services may be missing.
- **DNS can fail.** If DNS does not answer for a domain, the row is `unknown` and free. If DNS fails for more than half of the domains checked (after 10 domains, or after 30 seconds) or for more than half of the first 100 addresses, the run stops with an error so you do not wait for a list of dead domains.

### Troubleshooting

#### Many rows are `unknown`

Symptom: rows with `dns_lookup_failed`. Likely reason: the domains' DNS servers do not answer, or there was a short network problem. Action: run again with only the `unknown` addresses. They were not charged.

#### The run failed with "DNS lookups failed…"

Symptom: the run ends with an error after about 100 addresses. Likely reason: most domains in the list do not answer DNS. Action: check that the list has real domains. Rows already checked are in the dataset.

#### The run failed with "No email addresses found"

Symptom: the run fails at once. Likely reason: `emails` is empty, or the dataset field in `emailField` has another name. Action: open the dataset, find the field with the address, and put its name in `emailField`.

#### The run stopped with status PARTIAL

Symptom: `RUN_SUMMARY` shows `PARTIAL` and `notProcessed` > 0. Likely reason: the run reached your maximum cost or its time limit. Action: raise the limit, or run the remaining addresses in a new run.

### FAQ

#### Do I need an account or API key from an email verification service?

No. Everything runs inside the Actor. You only need an Apify account.

#### Why is a fake gmail.com address "deliverable"?

Because the Actor does not ask the mail server if the mailbox exists (no SMTP). It can only say that gmail.com accepts mail. The `mailbox_not_verified` reason shows this.

#### Is my email list stored or shared?

The addresses are only saved in your run's dataset. They are not logged, not sent to a verification API, and not kept after the run by the Actor. Domain names are looked up in public DNS.

#### Should I delete every `risky` address?

Not always. Role addresses like `sales@` are often real. Disposable addresses and typos are usually worth removing or fixing.

### Support

Open an issue in the **Issues** tab of this Actor. Include the run ID and the error message. Do not post your full email list or any tokens.

# Changelog

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

# Actor input Schema

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

Up to 10,000 addresses per run. One per line; a pasted line with several addresses separated by commas, semicolons or spaces is split automatically. Duplicates (case-insensitive) are checked once.

## `inputDatasetId` (type: `string`):

Pick an existing Apify dataset to read addresses from, e.g. the output of a lead scraper, or pass its ID in the API. Combined with the list above; 10,000 addresses at most in total. Up to 20,000 dataset items are read; use a smaller dataset if most items have no address.

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

Name of the field that holds the address in the input dataset. Rows without it are counted in the run summary.

## `concurrency` (type: `integer`):

How many addresses are checked at the same time (5–50). DNS results are shared per domain, so lists with many addresses on one domain are fast at any value.

## `deduplicate` (type: `boolean`):

Check each address once (case-insensitive). Turn off to keep one output row per input line.

## Actor input object example

```json
{
  "emails": [
    "support@apify.com",
    "someone@nonexistentdomain-7c1e5b42.com",
    "test@mailinator.com",
    "sales@stripe.com",
    "john..doe@example.com"
  ],
  "emailField": "email",
  "concurrency": 20,
  "deduplicate": true
}
```

# Actor output Schema

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

Verification results as a table.

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

Status, counts by result, charged rows and notes.

# 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": [
        "support@apify.com",
        "someone@nonexistentdomain-7c1e5b42.com",
        "test@mailinator.com",
        "sales@stripe.com",
        "john..doe@example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cliqtomedia/email-verifier").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": [
        "support@apify.com",
        "someone@nonexistentdomain-7c1e5b42.com",
        "test@mailinator.com",
        "sales@stripe.com",
        "john..doe@example.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("cliqtomedia/email-verifier").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": [
    "support@apify.com",
    "someone@nonexistentdomain-7c1e5b42.com",
    "test@mailinator.com",
    "sales@stripe.com",
    "john..doe@example.com"
  ]
}' |
apify call cliqtomedia/email-verifier --silent --output-dataset

```

## MCP server setup

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

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/wwI7dqeyTLBR7bGwi/builds/JR7DAVRoNJ27JHZFd/openapi.json
