# Bulk Email Verifier - Validate & Clean Email Lists (`theprojectdesk/email-verifier`) Actor

Check a list of email addresses before you send. Catches dead domains, bad syntax, typos, disposable addresses and catch-all servers, and tells you which mail platform each domain runs. Built to run straight after a contact or lead scrape.

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

## Pricing

from $2.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bulk Email Verifier — validate and clean an email list before you send

Give it a list of email addresses. Get back a **deliverability verdict for every one** —
as a clean table you can export to CSV, Excel or JSON, or push straight into your sending
tool.

It exists because a scraped list is never a send list. Somewhere between 10% and 30% of
the addresses on one are dead domains, typos, shared inboxes or throwaway mailboxes, and
every one of them is a bounce. Enough bounces and your domain reputation goes with them.

***

### What it checks

Each address goes through five layers, cheapest first, and stops as soon as there is a
definite answer.

| Layer | Catches |
|---|---|
| **Syntax** | Malformed addresses, illegal characters, over-length local parts, missing or bogus TLDs |
| **Typo detection** | `sid@gmial.com` → *did you mean `sid@gmail.com`* — transposition-aware, so the most common real typo is caught |
| **Domain** | Domains that do not exist, and domains that publish a null MX (RFC 7505 — "this domain never receives mail") |
| **Mail server** | MX records, with the RFC 5321 fallback to an A record, so small domains that really do receive mail are not thrown away |
| **Mailbox** *(optional)* | An SMTP conversation as far as `RCPT TO` — **no message is ever sent** — plus catch-all detection so a meaningless `250` is not read as a real mailbox |

#### Things most verifiers get wrong, and this one handles

- **Catch-all servers.** A domain that accepts *every* address will happily accept a
  mailbox that does not exist. We probe a random address first, and if it is accepted the
  whole domain is marked `isCatchAll` and scored as **risky** rather than sold to you as
  deliverable.
- **Gmail normalization.** `J.Smith+news@gmail.com` and `jsmith@gmail.com` are one
  mailbox. The `normalizedEmail` column collapses them, so you can deduplicate a list
  properly instead of paying to mail the same person twice.
- **Role vs personal.** `info@` and `sales@` reach a shared inbox. They are still valid,
  so they are still `deliverable` — but they get their own `isRoleAccount` column, because
  cold outreach usually excludes them and a silent drop would be the wrong call to make on
  your behalf.
- **Public suffixes are not disposable.** Auto-generated blocklists tend to swallow
  `co.uk` and `edu.pl` because throwaway services hand out subdomains on them. Flagging
  every UK business as disposable is a far more expensive mistake than missing a
  throwaway, so the parent-domain list here is curated by hand.
- **Honest `unknown`.** Where the mailbox could not be proven, the status is `unknown` —
  not "probably fine". See the note on port 25 below.
- **The mail platform.** Free by-product of the MX lookup: Google Workspace, Microsoft
  365, Zoho, Proofpoint, Mimecast and 25 others, identified per domain.

***

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `emails` | array | — | The addresses. Paste one per line, upload a CSV, or link another Actor's dataset. Display names and `mailto:` prefixes are handled. |
| `smtpCheck` | boolean | `true` | Probe the mailbox over SMTP. See the note below. |
| `onlyKeepSendable` | boolean | `false` | Drop `undeliverable` rows so the export is already a clean send list. |
| `maxEmails` | integer | `0` | Safety cap while testing. `0` = no limit. |
| `maxConcurrency` | integer | `20` | Addresses handled at once. |
| `dnsTimeout` | integer | `5` | Seconds to wait for an MX record. |
| `smtpTimeout` | integer | `8` | Seconds to wait on the mail server. |
| `heloDomain` | string | `example.com` | The domain the probe introduces itself with. Set it to one you own for large lists — servers trust a real domain more. |
| `mailFrom` | string | `verify@example.com` | Return address used during the probe. Nothing is ever sent to it. |

**Minimal input:**

```json
{
  "emails": [
    "info@apify.com",
    "sid@gmial.com",
    "test@mailinator.com",
    "nobody@nosuchdomain-zzq7.com"
  ]
}
```

***

### Output

One row per address.

```json
{
  "email": "sid@gmial.com",
  "normalizedEmail": "sid@gmial.com",
  "status": "risky",
  "score": 30,
  "reason": "likely_typo",
  "localPart": "sid",
  "domain": "gmial.com",
  "isRoleAccount": false,
  "isFreeProvider": false,
  "isDisposable": true,
  "isCatchAll": false,
  "hasMxRecord": true,
  "mxProvider": "Other",
  "mxHost": "mx.gmial.com",
  "didYouMean": "sid@gmail.com",
  "checkLevel": "dns",
  "smtpDetail": ""
}
```

#### The `status` column

Four values, and they are the one thing in this Actor that is never fudged.

| Status | Means | What to do |
|---|---|---|
| `deliverable` | The mailbox exists and accepted the address | Send |
| `risky` | Real domain, but something is wrong with it — catch-all, disposable, or a likely typo | Send at your own risk, or fix the typo first |
| `undeliverable` | Guaranteed bounce — bad syntax, dead domain, null MX, or the server rejected the mailbox | Remove |
| `unknown` | The domain is fine but the mailbox could not be proven | Your call — `score` 65 means everything short of the mailbox check passed |

`score` is 0–100 and is there for sorting; `reason` is a stable machine-readable code you
can filter on.

***

### A straight note about port 25 and `unknown`

The mailbox probe needs outbound **port 25**, and many networks block it. When that
happens the Actor:

1. notices after a handful of domains,
2. **stops probing** rather than burning your compute on timeouts that all say the same
   thing, and
3. returns `unknown` with `reason: mailbox_unverified_mx_valid` for the rest.

**A DNS-level run is still worth running.** It removes bad syntax, dead domains, null-MX
domains, typos and disposable addresses — which on a scraped list is the large majority of
what would have bounced. What it cannot tell you is whether one specific mailbox at a live
domain exists. The Actor says so instead of guessing, because a verifier that quietly
upgrades "I could not check" to "deliverable" is worse than no verifier at all.

***

### Where this sits in a pipeline

```
Google Maps / directory scrape   →   company names + websites
            ↓
Website Contact Extractor        →   emails, phones, social profiles
            ↓
Bulk Email Verifier  (you are here)  →   a list that is safe to send to
```

Link the dataset from
[**Website Contact Extractor**](https://apify.com/theprojectdesk/website-contact-extractor)
directly into the `emails` field — its `emails` and `emailsRole` columns are read
automatically, so no reshaping is needed in between.

***

### Pricing

**$1.00 per 1,000 emails** ($0.001 each). No monthly fee, no minimum, nothing to cancel —
you pay for the addresses you actually put through it, and a failed check costs the same
as a successful one because the work happens either way.

For comparison, the mainstream web services charge $3–$16 per 1,000 for the same job.

***

### Run it locally first

No Apify account needed:

```bash
python tools/try_local.py info@apify.com sid@gmial.com test@mailinator.com
```

Add `--no-smtp` for DNS-level checks only, or `--file list.txt` to read a file.

Unit tests, no network required:

```bash
python tests/test_verify.py
```

***

### Notes

- The disposable-domain list is bundled from
  [martenson/disposable-email-domains](https://github.com/martenson/disposable-email-domains)
  (CC0). Refresh it with `python tools/refresh_disposable.py`.
- No message is ever sent to any address you submit. The SMTP conversation stops at
  `RCPT TO` and issues `QUIT`.
- Addresses you submit are processed for the run and written only to your own dataset.

# Actor input Schema

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

The addresses to check. Paste them one per line, upload a CSV, or link the dataset from another Actor - the output of Website Contact Extractor drops straight in. Display names like `"Sneha Rao" <sneha@acme.com>` are fine.

## `smtpCheck` (type: `boolean`):

Ask the mail server whether the mailbox exists, without sending anything. Turns 'unknown' into a real deliverable/undeliverable answer. If outbound port 25 is blocked on the network, the Actor detects it after a few tries, stops probing, and falls back to DNS-level results rather than burning your compute.

## `onlyKeepSendable` (type: `boolean`):

Drop every `undeliverable` row so the dataset you export is already a clean send list. Off by default - most people want the full picture, and the rejects are the audit trail.

## `maxEmails` (type: `integer`):

A safety cap while you are testing. 0 means no limit.

## `maxConcurrency` (type: `integer`):

How many addresses to work on at once. Lower it if a mail server starts rate-limiting you.

## `dnsTimeout` (type: `integer`):

How long to wait for a domain's MX record.

## `smtpTimeout` (type: `integer`):

How long to wait on the mail server. Only used when the mailbox probe is on.

## `heloDomain` (type: `string`):

The domain the probe introduces itself with. Mail servers trust a real domain more than a placeholder, so set this to one you own if you are checking a large list.

## `mailFrom` (type: `string`):

The return address used during the probe. No message is ever sent to it.

## Actor input object example

```json
{
  "emails": [
    "info@apify.com",
    "sid@gmial.com",
    "test@mailinator.com",
    "nobody@nosuchdomain-zzq7.com"
  ],
  "smtpCheck": true,
  "onlyKeepSendable": false,
  "maxEmails": 0,
  "maxConcurrency": 20,
  "dnsTimeout": 5,
  "smtpTimeout": 8,
  "heloDomain": "example.com",
  "mailFrom": "verify@example.com"
}
```

# Actor output Schema

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

Every row as JSON - status, score, reason and flags per address.

## `resultsCsv` (type: `string`):

The same rows as a spreadsheet, ready for a CRM or sending tool.

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

Counts of deliverable / risky / undeliverable / unknown for this run.

# 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",
        "sid@gmial.com",
        "test@mailinator.com",
        "nobody@nosuchdomain-zzq7.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("theprojectdesk/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": [
        "info@apify.com",
        "sid@gmial.com",
        "test@mailinator.com",
        "nobody@nosuchdomain-zzq7.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("theprojectdesk/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": [
    "info@apify.com",
    "sid@gmial.com",
    "test@mailinator.com",
    "nobody@nosuchdomain-zzq7.com"
  ]
}' |
apify call theprojectdesk/email-verifier --silent --output-dataset

```

## MCP server setup

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