# Email List Cleaner - Bulk Email Validator & Disposable Check (`tidytools/bulk-email-validator`) Actor

Email list cleaner and bulk email validator API without SMTP pings: validate syntax, domain and MX records, disposable, role and free-provider flags, and typos (gmial.com → gmail.com). CSV upload or another Actor's dataset. Failed lines are free. $0.50 per 1,000.

- **URL**: https://apify.com/tidytools/bulk-email-validator.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (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 e-mail validateds

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

### What does Email List Cleaner do?

**Email List Cleaner validates an email list in bulk: give it addresses, a CSV file or a lead scraper's dataset (in Apify Console or through the API), and get a verdict (valid, risky or invalid), a 0–100 score and a reason for every address from syntax, domain and MX, disposable, role and typo checks, without SMTP pings, for $0.50 per 1,000 addresses. Syntax errors, failed lookups and duplicates are free.**

#### How to validate an email list in bulk

1. Paste the addresses into **E-mail addresses**, upload a CSV file, or give the dataset of another run.
2. Click **Start**, or call the Actor from the API with `{"emails": [...]}`.
3. Download `VALID_EMAILS` (a ready-to-send clean list) or the full table as CSV, Excel or JSON. 10,000 addresses cost at most $5.

#### Why this one

- **$0.50 per 1,000 addresses** (Business plan and higher $0.40), with no start fee. The two most used e-mail verifiers on Apify Store charge $0.89 (bounceverify, with SMTP) and $100 (michael.g/email-verifier-validator) per 1,000 on the Free plan ([comparison](#price-comparison-checked-october-2026), checked October 2026).
- **Lines that cannot be checked are free**: syntax errors, DNS lookups that fail (`unknown`), merged duplicates and lines without an @. Every row says `charged: true/false`, and the run stops at your maximum charge per run.
- **No SMTP pings**: no mail server is contacted, so your IP cannot get blacklisted and the recipients are never touched. In exchange, the mailbox itself is not probed ([why](#tips-and-limitations)).
- **Any list shape, no API key**: paste addresses, upload a CSV or a published Google Sheet, or read the dataset of a contact or lead scraper; your own columns are copied next to each result, and `VALID_EMAILS` is a ready-to-send clean list.
- **Every address gets a verdict, a 0–100 score and a reason**, with disposable (about 9,200 domains), role, no-reply and free-provider flags and typo suggestions (`gmial.com` → `gmail.com`).

#### Input (copy and paste)

```json
{
    "emails": ["jane@apify.com", "someone@gmial.com", "zed@gmail.con", "bad..address@gmail.com"]
}
```

#### Output (real result, shortened)

Results from our test run of 1 October 2026. Three addresses had their domain checked and were charged ($0.0015 in total); the syntax error was free.

```json
[
    {
        "email": "jane@apify.com",
        "verdict": "valid",
        "score": 90,
        "reason": "domain accepts e-mail; mailbox not verified (no SMTP check)",
        "mailProvider": "Google",
        "disposable": false,
        "charged": true
    },
    { "email": "someone@gmial.com", "verdict": "risky", "score": 20, "reason": "disposable (temporary) e-mail domain", "didYouMean": "someone@gmail.com", "charged": true },
    { "email": "zed@gmail.con", "verdict": "invalid", "reason": "the domain does not exist (NXDOMAIN)", "didYouMean": "zed@gmail.com", "charged": true },
    { "email": "bad..address@gmail.com", "verdict": "invalid", "reason": "invalid syntax: consecutive dots in the local part", "charged": false }
]
```

All fields (MX records, flags, your CSV columns): [Output example](#output-example-from-a-csv-row-real-run-1-october-2026).

### How much does it cost?

| Event | Price |
|---|---|
| E-mail validated (domain checked) | **$0.50 per 1,000 addresses** ($0.0005 each) |

**No start fee.** Charged: each unique address whose domain was checked, whatever the verdict (`valid`, `risky` or `invalid`). **Not charged:** addresses with a syntax error (they never reach the DNS check), `unknown` results (DNS failed), duplicates merged by **Merge duplicate addresses**, and lines without an @ (empty CSV cells, notes). Every row says `charged: true/false`.

A 10,000-address list costs at most **$5** on the Free plan. Higher Apify plans get volume discounts: $0.45 per 1,000 on Scale, $0.40 on Business and higher (Starter pays the Free-plan price). The Apify platform usage of a run is small (about 256 MB of memory for the few seconds or minutes the run takes) and is included in the price.

### Features

It **cleans e-mail lists before you send**. For every address it checks the **syntax**, whether the **domain exists and accepts mail (MX records)**, and flags **disposable, role, no-reply and free-provider addresses** and **typos** such as `gmial.com` or `gmail.con`. Each address gets a **verdict** (`valid`, `risky`, `invalid`, `unknown`), a **score from 0 to 100** and a plain-English **reason**.

It **never connects to a mail server** (no SMTP "pings"), so it cannot get your IP blocked, it does not touch the recipients' mailboxes, and its answers do not depend on a mail server's mood. The trade-off is explained honestly below: a `valid` address has a domain that accepts mail and nothing suspicious, but the mailbox itself is not probed.

- ✅ **Syntax** per RFC 5321/5322: dot-atom and quoted local parts, internationalized domains, length limits, `"Jane Doe <jane@example.com>"` and `mailto:` accepted
- 🌐 **Domain and MX records** via DNS-over-HTTPS: non-existent domains (NXDOMAIN), **null MX** (domains that declare they take no mail), implicit MX fallback to A/AAAA
- 🗑️ **Disposable e-mail detection**: a list of about 9,200 temporary-mail domains (community CC0 list; its size and date are in `SUMMARY.disposableList`) plus disposable mail servers behind custom domains
- 👥 **Role addresses** (`info@`, `sales@`, `support@`…), **no-reply** mailboxes and **free providers** (Gmail, Outlook, Yahoo, GMX, QQ, Naver…)
- ✏️ **Typo suggestions**: `someone@gmial.com` → `didYouMean: "someone@gmail.com"`
- 🏢 **Mail provider** from the MX hosts: Google, Microsoft, Proofpoint, Mimecast, Zoho, Yandex and more
- 📥 **Any list shape**: a list field, pasted text, a **CSV upload** (the e-mail column is found automatically; other columns can be copied to the output) or the **dataset of another Actor** (e.g. a contact or lead scraper)
- 📤 **Ready-to-use clean lists**: besides the table, the run saves `VALID_EMAILS` and `VALID_AND_RISKY_EMAILS` as plain text, one address per line, and `READY_TO_SEND`, a CSV ranked deliverable → risky → undeliverable with the business columns of a lead dataset (`sendStatus`, `sendPriority`, `businessName`, `website`, `placeId`)
- 💵 **$0.50 per 1,000 addresses**. No start fee. Syntax errors, lookups that fail, duplicates and lines without an @ are free

### What each verdict means

| Verdict | When | Typical score | What to do |
|---|---|---|---|
| `valid` | The domain exists and accepts mail, and nothing looks wrong. Role addresses (`info@`) stay valid with a lower score | 60–90 | Keep |
| `risky` | Disposable domain, likely typo (see `didYouMean`), no-reply mailbox, or several weak signals (score under 60) | 0–55 | Fix the typo, or drop for cold outreach |
| `invalid` | Bad syntax, the domain does not exist, has a null MX record, or has no mail server at all | 0–5 | Remove |
| `unknown` | DNS did not answer, even after a retry | 50 | Run again later (not charged) |

`reasons` lists every signal behind the score, most important first; `reason` is the first one. `catchAll` is always `null` and `mailboxChecked` always `false`, because no mail server is contacted.

### Who is it for?

- **Marketers and sales teams** cleaning a list before a campaign, to cut bounces and protect the sender reputation
- **Lead-generation workflows** that scrape addresses (contact or Google Maps scrapers) and need to drop dead domains, disposable and role addresses
- **Product and growth teams** screening sign-up lists for throwaway e-mails and typos
- **Developers** who need an e-mail check inside an Apify, n8n, Make or Zapier pipeline

### Cost control and price comparison

#### Control your cost

- **What is charged:** one event per unique address whose domain was checked. Nothing else.
- **Max e-mails** (Advanced settings, default 100,000) caps the addresses validated per run; the rest are listed in `SUMMARY.notProcessed`.
- At the start, the run logs its worst case (e.g. `Plan: 12 e-mail addresses × $0.0005 = at most $0.006`) and warns when that is more than your **maximum charge per run**.
- When the maximum charge per run is reached, the run stops, keeps everything checked so far and says so. `SUMMARY` then has `status: "LIMIT_REACHED"` and `notProcessed` (count and up to 100 addresses): raise the limit and run those again.
- If the run is restarted by Apify (server migration or Resurrect), addresses already done are skipped and **not charged again**.

#### Price comparison (checked October 2026)

For comparison (checked October 2026, Apify Store prices on the Free plan, from the Store API): Actors that also probe mailboxes over SMTP cost $0.85–$1 per 1,000 at the low end (blessiticus/email-verifier-pro $0.85, bounceverify $0.89, amr-mando and snipercoder $1.00) and more elsewhere (fatihtahta $2, ryanclinton $5, michael.g/email-verifier-validator $100). DNS-only tools like this one cost $0.24–$2 per 1,000 (waags $0.24; pink_comic, datascoutapi and saswave $0.50; flash_scraper $1; vulnv $2). What this one adds: CSV upload and dataset input, failed lines free, a stop at your maximum charge per run, and restarted runs that never charge an address twice. If you need mailbox-level (SMTP) verification, those tools do something this one deliberately does not.

### How to use it

1. Put your addresses in **E-mail addresses** (use Bulk edit to paste a list), **Or paste a list**, **Or upload a CSV file**, or give the **dataset** of another run. You can combine them.
2. For a CSV, the column named like "email" is used, or the column with the most addresses. Set **CSV column** to choose, and **Copy these source columns to the output** (e.g. `Name`, `Company`) to keep your columns next to each result.
3. Click **Start**. Export the **Addresses (overview)** table as CSV, Excel or JSON, or download the clean lists `VALID_EMAILS` / `VALID_AND_RISKY_EMAILS` from the run's Storage tab (also linked in the Output tab).

#### Input example

```json
{
    "emails": ["jane.doe@gmail.com", "info@stripe.com", "someone@gmial.com", "test@mailinator.com"],
    "csvFile": "https://docs.google.com/spreadsheets/d/e/<id>/pub?output=csv",
    "csvColumn": "Work Email",
    "keepFields": ["Name", "Company"]
}
```

From another Actor's results (e.g. a contact scraper whose rows have an `emails` array):

```json
{ "datasetId": "<dataset ID>", "datasetField": "emails", "keepFields": ["url"] }
```

#### Output example (from a CSV row, real run 1 October 2026)

```json
{
    "input": "jane@apify.com",
    "email": "jane@apify.com",
    "verdict": "valid",
    "score": 90,
    "reason": "domain accepts e-mail; mailbox not verified (no SMTP check)",
    "didYouMean": null,
    "syntaxValid": true,
    "domain": "apify.com",
    "domainExists": true,
    "hasMx": true,
    "acceptsMail": true,
    "mailProvider": "Google",
    "disposable": false,
    "role": false,
    "noReply": false,
    "freeProvider": false,
    "mxRecords": [{ "priority": 1, "host": "aspmx.l.google.com" }, { "priority": 5, "host": "alt1.aspmx.l.google.com" }],
    "catchAll": null,
    "mailboxChecked": false,
    "success": true,
    "charged": true,
    "source": "csv",
    "csvRow": 2,
    "duplicates": 1,
    "sourceFields": { "Name": "Jane", "Company": "Acme" }
}
```

A typo on a disposable-listed domain:

```json
{
    "input": "someone@gmial.com",
    "verdict": "risky",
    "score": 20,
    "reason": "disposable (temporary) e-mail domain",
    "didYouMean": "someone@gmail.com",
    "reasons": [
        "disposable (temporary) e-mail domain",
        "possible typo: did you mean gmail.com?",
        "no MX record: mail would go to the domain's A/AAAA address (unusual for real mailboxes)"
    ],
    "charged": true
}
```

Other real results from the same test: `user@example.com` → `invalid`, "the domain does not accept e-mail (null MX record)"; `zed@gmail.con` → `invalid`, "the domain does not exist (NXDOMAIN)", `didYouMean: "zed@gmail.com"`; `noreply@github.com` → `risky`, "no-reply or system mailbox"; `bad..address@gmail.com` → `invalid`, "invalid syntax: consecutive dots in the local part" (`charged: false`).

A line that is not an address (free):

```json
{ "input": "hello world", "success": false, "errorType": "invalid_input", "error": "not an e-mail address (no @) (not charged)", "charged": false }
```

The `SUMMARY` record has `status`, counts per verdict, `flags` (disposable, role, noReply, freeProvider, typoSuggestions, syntaxInvalid), `duplicatesMerged`, `invalidInputs`, which CSV column or dataset field was read, and the disposable list's size and date.

### Lead-cleaning workflow: Google Maps results to a ready-to-send list

For sales teams: Google Maps results (or a list of domains) → the emails published on each business's own website → this validator → a ranked list for your CRM or outreach tool.

1. Run [Google Maps Email Extractor](https://apify.com/tidytools/website-contact-extractor) (`tidytools/website-contact-extractor`) on your Google Maps scraper's dataset or on a domain list, with `"requiredContact": "email"` so only websites where an email was found are charged ($2 per 1,000).
2. In that Actor's (or task's) **Integrations** tab choose **Connect Actor or task**, pick this Actor and use this input (`{{resource.defaultDatasetId}}` becomes the dataset of the run that just finished):

```json
{ "datasetId": "{{resource.defaultDatasetId}}" }
```

3. Open the run's **Ready to send** output: the `READY_TO_SEND` CSV is already sorted (deliverable, then risky, then undeliverable; each business's best email first) and keeps `businessName`, `website`, `placeId` and `rowId`, so it imports into a CRM or merges back into your Maps export by `placeId`.

Without the Console integration: an Apify webhook on `ACTOR.RUN.SUCCEEDED` of the extractor run that starts this Actor with the same `datasetId`, or one n8n flow (Google Sheet of domains → extractor → validator → a "Ready to send" sheet tab).

What is read from the extractor's rows: every address in `emails` (set **Dataset field** to `bestEmail` to check only the one best address per business, which costs less), and each address keeps its row's `businessName`, `placeId`, `website`, `rowId` and `isBestEmail` (true when it is the extractor's `bestEmail` for that business).

**Then add company data (optional).** Connect [Lead & Company Enrichment](https://apify.com/tidytools/company-website-enrichment) (`tidytools/company-website-enrichment`, $0.006 per company) after this Actor the same way (`{ "datasetId": "{{resource.defaultDatasetId}}" }`): each validated address keeps its row (email, `verdict`, `sendStatus`, `sendPriority`, `businessName`, `placeId`, `website`) and gets what the company does, its industry, B2B/B2C, the employee band when the company states it, and the company's social profiles. Several addresses of one business are read and charged once; an address without a website uses its company domain (never gmail.com and the like). On the test below: 35 rows in, 18 companies charged, $0.108.

**Real numbers (our test run, 4 October 2026):** 20 public websites of small food businesses (bakeries, delis and restaurants in the US and one in Munich). The extractor read all 20 in 26 seconds and found at least one email on 18 (35 addresses); this validator checked the 35 addresses in 2 seconds. All 35 came back `deliverable` (18 of them role addresses such as `info@` or `hello@`, 1 a Gmail address), so on this list nothing had to be removed. Cost: $0.04 for the websites (all 20 charged in the default mode; `"requiredContact": "email"` would have charged the 18 with an email, $0.036) plus $0.0175 for the addresses, **about $0.06 in total**. Fresh addresses published on live business websites rarely bounce at the domain level; the validator earns its cost on older lists, typed-in addresses and domains that have since expired. Across our 200-website benchmark the extractor found an email on about 67% of websites, so expect fewer than 18 of 20 on an average Maps list.

### Integrations

- **After a contact or lead scraper**: pass its dataset with `datasetId` (and `datasetField`, e.g. `emails` or `primaryEmail`). Arrays of addresses are expanded; `keepFields` carries the company URL or name along. With TidyTools' [Google Maps Email Extractor](https://apify.com/tidytools/website-contact-extractor) (`tidytools/website-contact-extractor`) the business columns are carried automatically (see [Lead-cleaning workflow](#lead-cleaning-workflow-google-maps-results-to-a-ready-to-send-list)).
- **After Places Database** (`tidytools/places-database`): pass its dataset with `datasetId`. Both `emails` (from the source) and `enrichedEmails` (found on the business website) are checked, duplicates are merged, and each address keeps the place's `name` as `businessName` and its `website`.
- **Google Sheets**: publish the sheet as CSV ("File → Share → Publish to web → CSV") and paste the link into **Or upload a CSV file**.
- **API and webhooks**: start runs and read the dataset or the `VALID_EMAILS` record from any language; use a webhook when a run finishes.
- **Apify integrations**: send results to Google Sheets, HubSpot, Slack, Zapier, Make, n8n and others from the Integrations tab.

### Use with AI agents (MCP)

Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/bulk-email-validator) to Claude, Cursor or any MCP client, then ask e.g. "Which of these sign-up e-mails are disposable or mistyped?"

```json
{ "emails": ["jane.doe@gmail.com", "someone@gmial.com", "test@mailinator.com"] }
```

Rows that could not be checked are not charged and carry an `errorType`; the `SUMMARY` record has `status` (`SUCCESS`, `PARTIAL_RESULTS`, `LIMIT_REACHED`, `FAILED`, `NO_RESULTS`, `INPUT_ERROR`). A run with no usable address (empty input, only text without an @, an unreadable CSV or dataset) still ends as a normal, free run: `SUMMARY.status` is `NO_RESULTS` or `INPUT_ERROR` and `SUMMARY.message` / `inputErrors` say why, so check `status` instead of the run state.

### Tips and limitations

- **No SMTP mailbox check, on purpose.** `valid` means "the domain accepts mail and nothing looks wrong", not "this mailbox exists". A made-up name at a real company domain (`nobody123@stripe.com`) is `valid`. SMTP probing is blocked or answered with "accept all" by most large providers (Gmail, Outlook, Yahoo, Proofpoint…), can get the checking IP blacklisted, and some providers count it as abuse; this tool avoids it. Catch-all detection needs SMTP, so `catchAll` is always `null`.
- **Role addresses stay valid** (with a lower score): `info@` or `sales@` often do receive mail. Filter on `role` if you only want people.
- **Free providers are not risky**: `freeProvider: true` just tells you it is a consumer mailbox, useful to separate B2B leads.
- **Typo suggestions** cover popular mailbox domains (Gmail, Yahoo, Hotmail, Outlook, iCloud, GMX…) and slipped endings (`.con`, `.cmo`, `.co` for `.com`). Real domains one letter away from a popular one are not flagged unless the name is long enough to be a clear typo.
- **Disposable list**: a large community list catches common throwaway services; a brand-new temporary domain may not be on it yet.
- **DNS answers are cached** for a short time, so re-running the same list gives the same result quickly. `unknown` results are retried once in the run and are free.
- **If our validation service is unavailable** (rare), the affected rows have `verdict: null`, `success: false`, an `errorType` and `charged: false`, and `SUMMARY.status` is `FAILED` or `PARTIAL_RESULTS`: run those addresses again later.
- **Output spelling:** `input` is your line as typed; `email` is the cleaned address (the part after the @ in lower case, `mailto:` and `"Name <...>"` removed, internationalized domains also shown as `domainUnicode`).
- **Excel files**: save as CSV first. The CSV can use commas, semicolons or tabs; a cell may hold several addresses separated by `;` or `,`.

### Responsible use

- **What is checked:** only public DNS records of the address's domain (MX, A/AAAA) through DNS-over-HTTPS, plus local rules. **No e-mail is sent and no mail server is contacted**, so the people behind the addresses are never touched.
- **What is stored:** the results stay in your own Apify run storage. The addresses are sent to our validation backend only to be checked; they are not stored there, request contents are not logged, and they are not used for anything else (only anonymous usage counts are kept).
- **Your list, your responsibility:** validate only addresses you are allowed to process. Having a `valid` address does not give consent to e-mail it: follow anti-spam and data-protection laws (GDPR, CAN-SPAM, CASL) and honour unsubscribes.
- Do not use the results to guess or harvest addresses of people who have not shared them with you.

### FAQ

**Does the Actor collect usage data?** The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.

**Does it check whether the mailbox really exists?** No. It checks everything that can be known without talking to the mail server (syntax, domain, MX, disposable, role, typos). See Tips and limitations for why SMTP probing is avoided. Use the verdict to remove what is certainly bad (`invalid`) and review what is doubtful (`risky`).

**Why is a Gmail address "valid" when the person may not exist?** Gmail accepts mail for gmail.com and the address has no suspicious signal. Gmail does not reveal whether a mailbox exists to outside checks.

**Am I charged for bad lines in my CSV?** No. Empty cells, headers and text without an @ get a free `invalid_input` row; syntax errors are free too. Every row has `charged` so you can check.

**What if I start it with an empty form or a wrong file link?** The run ends normally, charges nothing and tells you why: `SUMMARY.status` is `NO_RESULTS` (no address in the input) or `INPUT_ERROR` (the CSV link or dataset could not be read; see `SUMMARY.inputErrors`), and the status message says the same. Fix the input and start again.

**What happens with duplicates?** With **Merge duplicate addresses** on (default), each address is checked and charged once, ignoring upper/lower case; the row's `duplicates` says how many more times it appeared. Turn it off to get one row per input line.

**How do I keep my own columns (name, company) next to the result?** List them in **Copy these source columns to the output**; they appear in `sourceFields` on each row. CSV rows also carry `csvRow` (the line number in your file) and dataset rows `datasetItemIndex`.

**Can I get just the clean list?** Yes: the key-value records `VALID_EMAILS` and `VALID_AND_RISKY_EMAILS` hold one address per line, and `READY_TO_SEND` is the whole list as a CSV sorted for sending (`sendStatus`: `deliverable`, `risky`, `undeliverable`, `not_checked`). "Deliverable" means the domain accepts mail and nothing looks wrong; the mailbox itself is not probed.

**What are the limits?** Up to 100,000 addresses per run by default; raise **Max e-mails** (Advanced settings) to up to 1,000,000. Addresses over the limit, or over your maximum charge per run, are not processed or charged and are listed in `SUMMARY.notProcessed`. Each entry can be up to 320 characters.

**How fast is it?** Addresses are checked in batches of 50, with 4 batches at the same time by default (up to 10 with **Parallel batches**). DNS answers are cached, so most lists finish in seconds or minutes.

**Is it legal to validate e-mail addresses?** The Actor reads only public DNS records of each domain and applies local rules; it sends no e-mail and contacts no mail server. Validate only lists you are allowed to process, and follow anti-spam and data-protection laws when you send (see [Responsible use](#responsible-use)). This is not legal advice.

**Does it work with the API, webhooks, Make, n8n or Zapier?** Yes. Start runs from the Apify API, a schedule, a webhook or the Apify apps for Make, n8n and Zapier, chain it after a contact scraper with `datasetId`, or use it from AI agents through [MCP](#use-with-ai-agents-mcp). See [Integrations](#integrations).

Found this useful? A short review on the Store helps other users find it.

# Actor input Schema

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

One address per line (use Bulk edit to paste a whole list). "Jane Doe <jane@example.com>" and mailto: links are accepted. Lines without an @ get a free error row.

## `emailsText` (type: `string`):

Addresses separated by new lines, commas, semicolons or tabs, e.g. a column copied from a spreadsheet.

## `csvFile` (type: `string`):

A CSV or TSV file (up to 50 MB), or a link to one (e.g. a Google Sheets "Publish to the web" CSV link). The column named like "email" is used, or the column with the most addresses; set "CSV column" to choose. Excel files: save as CSV first.

## `csvColumn` (type: `string`):

Header name (e.g. "Work Email") or column number (1 = first) of the addresses in the CSV file. Empty = detect automatically.

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

ID of a dataset from another run, e.g. a contact or lead scraper. Every address in the chosen field is validated (fields holding several addresses, such as an "emails" array, are expanded). With the dataset of TidyTools' Google Maps Email Extractor (website-contact-extractor), every address found on each business's website is checked and keeps the row's businessName, placeId, website, rowId and isBestEmail, so the "Ready to send" list merges back into your Maps export or sheet.

## `datasetField` (type: `string`):

Field with the addresses (dot paths allowed, e.g. "contact.email"). Empty = the first field named like email/emails/bestEmail/primaryEmail, or the field whose values contain addresses. For Google Maps Email Extractor results that is "emails" (every address found); set "bestEmail" to check only the one best address per business (cheaper).

## `keepFields` (type: `array`):

For CSV and dataset input: column or field names to copy into each row's sourceFields (e.g. "name", "company"), so the result maps back to your list.

## `dedupe` (type: `boolean`):

Validate (and charge) each address once, ignoring upper/lower case. The row gets duplicates = how many more times the address appeared. Turn off to get one row per input line.

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

Cost cap: at most this many unique addresses are validated per run ($0.50 per 1,000 on the Free plan). The rest are listed in SUMMARY.notProcessed.

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

Batches of 50 addresses checked at the same time. Higher values finish big lists sooner; the price is the same.

## Actor input object example

```json
{
  "emails": [
    "jane.doe@gmail.com",
    "info@stripe.com",
    "sales@apify.com",
    "someone@gmial.com",
    "test@mailinator.com",
    "noreply@github.com",
    "user@example.com",
    "bad..address@gmail.com"
  ],
  "keepFields": [],
  "dedupe": true,
  "maxEmails": 100000,
  "maxConcurrency": 4
}
```

# Actor output Schema

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

No description

## `readyToSend` (type: `string`):

No description

## `readyToSendView` (type: `string`):

No description

## `validEmails` (type: `string`):

No description

## `validAndRisky` (type: `string`):

No description

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

No description

## `full` (type: `string`):

No description

## `allColumns` (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": [
        "jane.doe@gmail.com",
        "info@stripe.com",
        "sales@apify.com",
        "someone@gmial.com",
        "test@mailinator.com",
        "noreply@github.com",
        "user@example.com",
        "bad..address@gmail.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/bulk-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": [
        "jane.doe@gmail.com",
        "info@stripe.com",
        "sales@apify.com",
        "someone@gmial.com",
        "test@mailinator.com",
        "noreply@github.com",
        "user@example.com",
        "bad..address@gmail.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tidytools/bulk-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": [
    "jane.doe@gmail.com",
    "info@stripe.com",
    "sales@apify.com",
    "someone@gmial.com",
    "test@mailinator.com",
    "noreply@github.com",
    "user@example.com",
    "bad..address@gmail.com"
  ]
}' |
apify call tidytools/bulk-email-validator --silent --output-dataset

```

## MCP server setup

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