# Bulk Email Verifier — CSV, Excel & List Cleaning (`mxxiv/email-verifier`) Actor

Upload a CSV or Excel file, paste a list, or chain another Actor's dataset — every address is sorted into valid, invalid or risky. Checks syntax, mail servers, disposable providers, role accounts and domain typos, and reports exactly which checks it could run.

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

## Pricing

from $1.00 / 1,000 email verifieds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

**Clean an email list from a CSV file, an Excel spreadsheet, a pasted list or another Actor's dataset** — before you send to it. This Actor checks **syntax, mail-server configuration, throwaway providers, shared team mailboxes and likely typos**, and tells you plainly which of those checks it was able to run for every single address.

### What does this bulk email verifier do?

It takes a list of email addresses and sorts them into **valid**, **invalid**, **risky** and **unknown**, with a reason and a confidence score attached to each verdict. It catches malformed addresses, domains that publish no mail server at all, throwaway providers like Mailinator, shared mailboxes like `info@` and `support@`, and near-miss typos such as `gmial.com` where it suggests the correction.

**Feed it whatever you already have.** Upload a **CSV or Excel file**, paste a list, or point it at another Actor's **dataset** — chain it straight onto a lead scraper and skip the export-and-re-import step entirely. The address column is detected automatically, whether it is called `email`, `work_email` or `Contact E-Mail`. Most verifiers on this Store accept a JSON array and nothing else; if your list already lives in a spreadsheet, that is a manual conversion step every single time.

It runs on the Apify platform, so you can call it from the API, schedule it, or pipe another Actor's output straight into it. Results download as JSON, CSV, Excel or HTML.

**It does not send email.** Nothing in this Actor delivers a message to anyone on your list.

### Why clean an email list before you send?

- **Protect your sender reputation.** Bounces and spam-trap hits are what get a sending domain throttled or blocked. Removing definitively bad addresses before a campaign is the cheapest reputation insurance there is.
- **Stop paying to store dead leads.** Most CRMs and sending platforms bill per contact.
- **Filter role accounts out of cold outreach.** `sales@`, `info@` and `admin@` reach a shared inbox, convert poorly, and count against you on several sending platforms.
- **Catch typos while the lead is still fresh.** A signup that typed `gmial.com` is a real person you can still recover, not a dead record.

### Honest limits — please read before you buy

**This Actor does not confirm that an individual mailbox exists.** Doing that requires an SMTP conversation with the recipient's mail server on port 25, and the Apify platform blocks outbound port 25 — as do most cloud providers and many consumer ISPs. Large mailbox providers also refuse these probes from datacenter IP ranges regardless.

Rather than pretend otherwise, every result carries a `smtpChecked` flag and a `confidence` score, so you always know how much was actually established:

| Verdict | What it really means | Confidence |
|---|---|---|
| `invalid` — `syntax_error` | Not a well-formed address. Certain. | 100 |
| `invalid` — `no_mail_server` | The domain publishes no mail server, so it cannot receive mail. Certain. | 95 |
| `risky` — `disposable_domain` | A throwaway mailbox provider. Certain. | 95 |
| `risky` — `role_account` | A shared team mailbox, not a person. Certain. | 85 |
| `valid` — `domain_verified_mailbox_unchecked` | Well-formed, and the domain accepts mail. **The specific mailbox was not probed.** | 70 |
| `unknown` — `dns_error` | A lookup failed. Says nothing about the address either way. | 0 |

So the strong guarantee is on the **negative** side: what this Actor marks `invalid` or `risky` is reliably bad and safe to drop. A `valid` result means "passed every check that could be run", not "a human is reading this inbox".

If you need mailbox-level confirmation, you need a service that operates its own port-25-enabled relay infrastructure. If you are running this Actor somewhere with port 25 open, switch on **Attempt SMTP mailbox probe** and it will use it, including catch-all detection.

### How to verify an email list from CSV, Excel or a dataset

1. Open the Actor and go to the **Input** tab.
2. Give it your addresses in whichever form you have them:
   - upload a spreadsheet under **CSV or Excel file**;
   - paste them into **Paste a list instead**, or add them one per row under **Email addresses**;
   - or pick a previous run's dataset under **Verify another Actor's dataset**.
3. Click **Start**.
4. When the run finishes, open the **Output** tab and download the results in your preferred format.

You can combine sources in one run — everything is merged and de-duplicated.

To run it on a schedule or from your own code, use the **API** tab — Apify generates ready-made snippets.

#### Verifying a CSV file or Excel spreadsheet

Drop the file into **CSV or Excel file** and start the run. There is nothing to configure:

- **Delimiters are detected** — comma, semicolon, tab or pipe.
- **The address column is found automatically**, whether the header says `email`, `Work Email` or `Contact E-Mail`. Set **Address field name** only if the sheet has several address columns and you want a specific one.
- **A header row is optional.** A single bare column of addresses works.
- **Files are identified by their contents, not their extension**, so a download link with no `.csv` on the end is fine.

Exporting from Google Sheets, Mailchimp, HubSpot or a CRM usually gives you a CSV already — that file goes in as-is. Legacy `.xls` is the one format not supported; re-save it as `.xlsx` or CSV.

#### Chaining it onto a lead scraper

Run any scraper that produces contacts, then select its dataset under **Verify another Actor's dataset**. Every item is scanned and its addresses verified — no export, no CSV round-trip. Leave **Address field name** blank and common keys are detected automatically; set it if your dataset has more than one address column and you want a specific one.

This Actor runs with **limited permissions**: it can read the one dataset you select and nothing else in your account.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `emails` | array | – | Addresses to verify. Accepts plain strings, or objects with an `email` field so another Actor's output pipes straight in. |
| `emailsRaw` | string | – | Paste addresses separated by newlines, commas or semicolons. Merged with `emails` and de-duplicated. |
| `inputDatasetId` | string | – | Dataset from another Actor's run, chosen with the resource picker. Every item is scanned for addresses. |
| `fileUrl` | string | – | Uploaded `.csv` or `.xlsx` file, or a link to one. |
| `emailField` | string | – | Name of the field or column holding the address. Blank means auto-detect. Applies to both the dataset and the file. |
| `maxConcurrency` | integer | `20` | How many addresses to check in parallel. |
| `enableSmtpCheck` | boolean | `false` | Attempt a mailbox-level SMTP probe. Normally unavailable — see the limits section above. |
| `detectCatchAll` | boolean | `true` | When the SMTP probe works, also detect domains that accept every address. |
| `smtpTimeoutSeconds` | integer | `10` | Per-server timeout for the probe. |
| `heloDomain` | string | `example.com` | Domain announced during the SMTP handshake. |
| `mailFrom` | string | `verify@example.com` | Envelope sender for the probe. No message is ever sent. |

```json
{
  "emails": ["support@github.com", "someone@gmial.com", "test@mailinator.com"],
  "maxConcurrency": 20
}
```

Verifying a scraper's output instead:

```json
{
  "inputDatasetId": "aBcDeFgHiJkLmNoPq",
  "emailField": "work_email"
}
```

### Output

One record per address, in the order you supplied them:

```json
{
  "email": "someone@gmial.com",
  "normalizedEmail": "someone@gmial.com",
  "status": "risky",
  "deliverability": "risky",
  "score": 10,
  "subStatus": "disposable_domain",
  "confidence": 95,
  "reason": "address belongs to a throwaway/temporary mailbox provider",
  "localPart": "someone",
  "domain": "gmial.com",
  "isDisposable": true,
  "isRoleAccount": false,
  "isFreeProvider": false,
  "hasValidSyntax": true,
  "hasMailServer": null,
  "mxHost": null,
  "mxRecords": [],
  "didYouMean": "someone@gmail.com",
  "smtpChecked": false,
  "smtpOutcome": null,
  "smtpDetail": null,
  "checkedAt": "2026-08-01T11:06:24.918273+00:00"
}
```

Checks stop as soon as the verdict is settled — the example above is a throwaway
domain, so its mail servers were never looked up. Fields belonging to a check
that did not run are `null`, never `false`: **"not checked" and "checked, found
nothing" are different answers**, and only the second one is safe to filter on.

A `SUMMARY` record in the key-value store holds the run totals per status.

#### Data fields

| Field | Type | Description |
|---|---|---|
| `email` | string | The address exactly as you supplied it. |
| `normalizedEmail` | string | Trimmed and lowercased form. |
| `status` | string | `valid`, `invalid`, `risky` or `unknown`. |
| `deliverability` | string | The same verdict in the vocabulary other verifiers use: `good`, `risky`, `bad` or `unknown`. |
| `score` | integer | 0–100 **deliverability** score — how likely the address is to accept mail. |
| `subStatus` | string | The specific reason behind the status. |
| `confidence` | integer | 0–100 certainty **in the assigned status** — a confident `invalid` scores as high as a confident `valid`. Not the same axis as `score`: a malformed address is a certain verdict (`confidence` 100) about an address that will never deliver (`score` 0). |
| `reason` | string | Human-readable explanation. |
| `localPart` / `domain` | string | The two halves of the normalized address. |
| `isDisposable` | boolean | Throwaway mailbox provider. |
| `isRoleAccount` | boolean | Shared team mailbox (`info@`, `support@`, …). |
| `isFreeProvider` | boolean | Consumer webmail. A B2B signal, not a deliverability problem. |
| `hasValidSyntax` | boolean | Passed RFC-compliant syntax validation. |
| `hasMailServer` | boolean | Domain publishes an MX record, or an A/AAAA record usable as an implicit MX. `null` when the lookup did not run or could not answer — see the note above. |
| `mxHost` / `mxRecords` | string / array | Highest-priority mail server, and the full list. Empty when `hasMailServer` is `null`. |
| `didYouMean` | string | Suggested correction for a near-miss domain typo, else `null`. Only the part before the suffix is corrected, so real regional domains (`live.cn`, `gmx.net`, `yahoo.ca`) are left alone; a suffix typo (`gmail.con`) is suggested only once the domain is confirmed to accept no mail. |
| `smtpChecked` | boolean | **Whether the individual mailbox was actually probed.** Normally `false`. |
| `smtpOutcome` / `smtpDetail` | string | Probe result and explanation, when one ran. |
| `checkedAt` | string | ISO 8601 timestamp. |

### How much does it cost to verify emails?

| | Price |
|---|---|
| Per address verified | $0.001 (i.e. $1.00 per 1,000) |
| Per address returned as `unknown` | free |
| Per run started | $0.00005 |

An `unknown` result means a check failed in a way that established nothing about
the address — almost always a nameserver that did not answer. You are not billed
for those: you did not get a verification, so you do not pay for one. Setting a
`maxTotalChargeUsd` cap on the run is also honoured properly — the run stops
there rather than quietly finishing the rest for free on your compute.

**This Actor becomes free on 16 August 2026.** Pricing has been switched off and, per Apify's policy, the change takes effect after a two-week notice period. From that date there is no per-address charge at all — you pay only the Apify platform compute a run consumes.

It is cheap to run either way, because it does no web scraping: no browser is launched, no page is rendered, and DNS lookups are cached per domain. A list of 5,000 addresses spread across 200 domains costs 200 lookups, not 5,000, so large single-domain lists are especially efficient.

Measured on the Apify platform, 945 addresses at a 256 MB memory setting:

| | |
|---|---|
| Run time | 44.5 s |
| Peak memory | 82 MB |
| Compute consumed | 0.0031 compute units |

### How reliable is it?

On that same 945-address platform run, **0.53% of addresses came back `unknown`** — five addresses across four domains whose own nameservers were failing at the time. Everything else reached a definite verdict.

Two design choices keep that number low:

- **Fallback resolver.** If the primary resolver cannot answer, a second is asked before giving up. Only "the resolver could not tell us" errors are retried; an authoritative "this domain does not exist" is trusted immediately.
- **A failed lookup never becomes `invalid`.** A DNS timeout says nothing about an address, so it is reported as `unknown` rather than silently deleting a good contact from your list.

### Tips

- **De-duplicate first if you are cost-sensitive.** The Actor de-duplicates within a run, but not across separate runs.
- **Decide what to do with `risky` deliberately.** Dropping every role account is right for cold outreach and wrong for support or billing contacts. `isRoleAccount` and `isDisposable` are separate fields so you can filter on exactly what you mean.
- **Treat `unknown` as "retry later", not as "bad".** It usually means a DNS timeout, and it is not charged for.
- **Act on `didYouMean`.** A suggested correction is often a recoverable customer.
- **Sort on `score` and cut where you like.** `deliverability` gives you the three-bucket view; `score` lets you pick your own threshold.

### Code examples

Runnable Python examples live in a separate repository:
**[github.com/zhoulianchang/email-verifier-examples](https://github.com/zhoulianchang/email-verifier-examples)**

- `verify_list.py` — read a local CSV, verify, split into keep / drop / review
- `chain_from_actor.py` — pass an upstream Actor's dataset ID straight in, with no download or re-upload
- `samples/output-sample.json` — 14 records of real output covering all four statuses, so you can see the exact shape before writing any code

The examples treat `unknown` as its own bucket rather than folding it into "bad", which is the filtering mistake most likely to cost you good contacts.

### FAQ

**Does this send any email?**
No. Even with the SMTP probe enabled, the conversation stops at `RCPT TO`, before any message body is transmitted.

**Why is a well-known good address marked `risky`?**
Most likely it is a role account such as `info@` or `support@`. Check the `isRoleAccount` field. These are usually deliverable — they are flagged because they are a poor outreach target, not because they bounce.

**Why does `valid` only score 70 confidence?**
Because the mailbox itself could not be probed. See the limits section — that number is the honest one.

**Can it detect catch-all domains?**
Only when the SMTP probe is available, which on the Apify platform it is not.

**Which file formats can I upload?**
`.csv` (comma, semicolon, tab or pipe delimited — the delimiter is detected) and `.xlsx`. A single column of addresses with no header row works too. Legacy `.xls` is not supported; re-save it as `.xlsx` or CSV. Files are identified by their contents, not their extension, so a link with no extension is fine.

**Can I verify an email list exported from Google Sheets or Excel?**
Yes. Export the sheet as CSV or `.xlsx` and upload it under **CSV or Excel file**. No column mapping to set up — the address column is detected by name.

**How many addresses can it handle in one run?**
Tested at 5,000 in a single run at 39 MB peak memory, and 945 addresses on the platform in 44.5 seconds. There is no built-in ceiling; DNS lookups are cached per domain, so a large list concentrated in few domains is disproportionately fast.

**Can it run automatically on new leads?**
Yes — schedule it from the **Schedules** tab, or chain it onto a scraper so each new run's dataset is verified as it lands.

**My dataset has several address columns. Which one is used?**
Auto-detection prefers an exact match on common names (`email`, `work_email`, `contactEmail`, …). If you want a specific one, set **Address field name** — it is then honoured strictly, so you get an obvious empty result rather than a silent fallback if the name is wrong.

**Is the disposable-domain list kept current?**
It ships with the Actor image from the public-domain [disposable-email-domains](https://github.com/disposable-email-domains/disposable-email-domains) project and is refreshed when the Actor is rebuilt. Bundling it means a run never depends on a third-party host being reachable.

### Support

Found an address the Actor gets wrong? Open an issue on the **Issues** tab with the address and what you expected — misclassifications are the most useful bug reports for this Actor.

# Actor input Schema

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

Addresses to verify, one per item. Accepts plain strings, or objects with an "email" field so output from another Actor can be piped straight in.

## `emailsRaw` (type: `string`):

Paste addresses separated by newlines, commas or semicolons. Merged with the list above and de-duplicated.

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

Pick a dataset from a previous run — typically a lead scraper's output. Every item is scanned and its email addresses verified, so you can chain this Actor straight onto a scraper without exporting anything. Leave the field name blank and common keys (email, work\_email, contactEmail, …) are detected automatically.

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

Upload a .csv or .xlsx file, or paste a link to one. The address column is detected from its header; a single column of addresses with no header works too.

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

Name of the dataset field or spreadsheet column holding the address, e.g. "work\_email". Leave blank to auto-detect. Applies to both the dataset and the file above.

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

How many addresses to verify in parallel. Lookups for the same domain are cached and never duplicated, so large lists at few domains stay cheap.

## `enableSmtpCheck` (type: `boolean`):

Tries to confirm the individual mailbox by talking to the mail server on port 25. The Apify platform blocks outbound port 25, as do most clouds and many ISPs, so this normally cannot connect and results fall back to domain-level verification with smtpChecked=false. Leave off unless you are running this Actor somewhere with port 25 open.

## `detectCatchAll` (type: `boolean`):

When the SMTP probe is available, also test a random address at the same domain. If that is accepted too, the domain accepts everything and the result is reported as risky rather than valid.

## `smtpTimeoutSeconds` (type: `integer`):

How long to wait for the mail server before giving up on the probe.

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

Domain announced during the SMTP handshake. Some mail servers reject probes whose HELO domain does not resolve.

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

Envelope sender used during the SMTP probe. No message is ever sent — the transaction stops before DATA.

## Actor input object example

```json
{
  "emails": [
    "support@github.com",
    "someone@gmial.com",
    "test@mailinator.com",
    "nobody@this-domain-does-not-exist-9f3a.com"
  ],
  "maxConcurrency": 20,
  "enableSmtpCheck": false,
  "detectCatchAll": true,
  "smtpTimeoutSeconds": 10,
  "heloDomain": "example.com",
  "mailFrom": "verify@example.com"
}
```

# Actor output Schema

## `verifiedAddresses` (type: `string`):

One record per address, with its verdict, reason and the checks that were actually run. Download as JSON, CSV, Excel or HTML.

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

Totals per status for the whole run, including how many addresses were left unverified.

# 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@github.com",
        "someone@gmial.com",
        "test@mailinator.com",
        "nobody@this-domain-does-not-exist-9f3a.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("mxxiv/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@github.com",
        "someone@gmial.com",
        "test@mailinator.com",
        "nobody@this-domain-does-not-exist-9f3a.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("mxxiv/email-verifier").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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@github.com",
    "someone@gmial.com",
    "test@mailinator.com",
    "nobody@this-domain-does-not-exist-9f3a.com"
  ]
}' |
apify call mxxiv/email-verifier --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=mxxiv/email-verifier",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/6VU3Ca7Q2FrSfFQKb/builds/1giEv9gFIHpaXEDCc/openapi.json
