# Bulk Email Verifier $1/1k: CSV, Sheets & Datasets (`osel_house/email-list-verifier`) Actor

✅ $1 per 1,000 emails, no subscription ✅ Paste a list, pick a lead-scraper dataset, upload a CSV or share a Google Sheet ✅ Keeps every column ✅ Syntax, MX, mailbox, catch-all & disposable checks ✅ Duplicates and non-addresses free ✅ Output to dataset or Google Sheet ✅ Resumable, no double charge

- **URL**: https://apify.com/osel_house/email-list-verifier.md
- **Developed by:** [Tenzin Phuntsok](https://apify.com/osel_house) (community)
- **Categories:** Lead generation, Automation, Marketing
- **Stats:** 3 total users, 2 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/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

**Clean your email list before you send.** Paste addresses, point at a lead-scraper dataset, upload a CSV or share a Google Sheet, and get back every row with a clear verdict: **valid**, **risky** (catch-all or unknown) or **invalid** (no mailbox, dead domain, disposable, typo). Keeps every column you gave it, verifies each duplicate only once, skips blanks and non-addresses for free, and can write the cleaned list straight into a Google Sheet.

### What does Bulk Email Verifier do?

Email verification checks whether an address can receive mail **without sending anything**. For each address this Actor checks the format, looks up the domain's mail servers, asks the mail server whether the mailbox exists, and detects catch-all domains, disposable services and role addresses (`info@`, `sales@`). The mailbox check runs through [MillionVerifier](https://www.millionverifier.com/), an established verification service, so you get the same accuracy as the big list-cleaning tools at a fraction of the price and inside your Apify workflow (schedules, API, integrations, chaining with scrapers).

- **Three ways in**: paste a list; pick the dataset of another Actor (LinkedIn, Apollo, Google Maps or any lead scraper) and the email column is found automatically; upload a CSV/TXT or paste a Google Sheet link.
- **Every column kept**: name, company, phone and anything else travel with the address into the output, so the result is your enriched list, not just a list of emails.
- **Free pre-checks**: duplicates are verified once, blank cells and text that is not an address cost nothing, and a domain that has no mail server is decided locally in milliseconds (that verdict is still one charged address).
- **Plain-English reasons**: `reason` explains each verdict ("The mail server says this mailbox does not exist"), next to the technical `subresult` code.
- **Typo fixes**: `didYouMean` suggests `jane@gmail.com` for `jane@gmial.com`.
- **Output filter**: all rows, only valid ones, or valid plus risky.
- **Google Sheets**: tick a box and the results land in your sheet (via our [Dataset to Google Sheets Sync](https://apify.com/osel_house/dataset-to-google-sheets) Actor).
- **Fast and finishes**: around 900 addresses a minute with the default settings (measured on the platform); parallel checks, per-address timeout, automatic retries, a second look at "unknown" answers (greylisting), and a run that stops cleanly before the platform timeout with everything so far saved. If a run stops early, **Resurrect** it in Console and it continues where it left off without paying again for what was verified.

### Why verify email lists?

Mail providers judge you by your bounce rate. Above roughly 2% bounces, deliverability drops for everyone you write to, and cold-email tools suspend accounts. Scraped and purchased lists typically contain 10-30% dead addresses. Verifying first:

- protects your sending domain and keeps campaigns in the inbox,
- cuts the cost of tools that bill per contact,
- removes fake sign-ups and typos from forms and CRMs,
- lets you segment by risk (send to valid now, warm up risky separately).

### How to verify an email list

1. Open the **Input** tab. Paste addresses into **Email addresses** (one per line), **or** pick a dataset under **Dataset from another Actor**, **or** upload a file / paste a Google Sheet link under **CSV / TXT file or Google Sheet** (the sheet must be shared as "Anyone with the link can view").
2. Optionally choose **Which rows to output** (all, valid only, valid and risky).
3. Optionally paste a **Google Sheet link** under Output to receive the results in a sheet. Share that sheet as **Editor** with the service account shown in the [Dataset to Google Sheets Sync README](https://apify.com/osel_house/dataset-to-google-sheets) (the run log prints it too).
4. Click **Start**. The **Output** tab shows one row per input row; the **Storage > Key-value store > SUMMARY** record has the counts, warnings and the sheet status.
5. Download as CSV, Excel or JSON, or pipe the dataset into the next Actor.

To clean a scraper's output automatically, add this Actor as an [integration](https://docs.apify.com/platform/integrations) on the scraper's run or run both from a schedule.

### Input

| Field                                                 | What it does                                                                                                                                               |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `emails`                                              | Pasted addresses. `Jane <jane@x.com>` and `mailto:` links are understood.                                                                                  |
| `datasetId`                                           | A dataset with an email column. Other columns are kept.                                                                                                    |
| `emailField`                                          | Column name holding the address (nested `contact.email` works). Empty = detected automatically.                                                            |
| `fileUrl`                                             | Uploaded CSV/TXT (up to 20 MB), a public file URL, or a Google Sheet link (shared "anyone with the link"). Header row optional. XLSX: export to CSV first. |
| `outputFilter`                                        | `all` (default), `valid` (result `ok`), `valid-and-risky` (`ok`, `catch_all`, `unknown`).                                                                  |
| `keepInputFields`                                     | Copy the other columns into the output rows (default on).                                                                                                  |
| `googleSheetUrl`, `googleSheetTab`, `googleSheetMode` | Also write the results to a Google Sheet tab, replacing it or appending.                                                                                   |
| `recheckUnknown`                                      | Ask a second time when a server answered "try later" (default on, this Actor does not charge extra for it).                                                |
| `verificationTimeoutSecs`                             | Seconds to wait for a mail server per address (default 15, 2-60).                                                                                          |
| `maxConcurrency`                                      | Parallel checks (default 20). Speed depends mostly on how fast the mail servers answer.                                                                    |
| `maxEmails`                                           | Stop reading input after this many rows (default 100,000, max 500,000).                                                                                    |
| `millionVerifierApiKey`                               | Optional: your own MillionVerifier API key. Checks are paid from your credits and this Actor charges only a small platform fee per address.                |

### Output

One dataset row per input row, in input order. Your original columns come first, then the verification fields. You can download the dataset as JSON, CSV, Excel or HTML.

```json
{
    "Name": "Jane Doe",
    "Company": "Acme",
    "email": "jane.doe@acme.com",
    "result": "ok",
    "quality": "good",
    "subresult": "ok",
    "reason": "Mailbox exists and accepts mail",
    "isFree": false,
    "isRole": false,
    "didYouMean": null,
    "duplicateOf": null,
    "verifiedBy": "millionverifier",
    "verifiedAt": "2026-10-09T14:02:11.482Z",
    "error": null
}
```

| Field         | Meaning                                                                                                                                                                                                    |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `email`       | The address that was checked (trimmed, domain lower-cased). `emailInput` holds the original text when it differed.                                                                                         |
| `result`      | `ok` (deliverable), `catch_all` (domain accepts everything, mailbox unconfirmed), `unknown` (server did not answer conclusively), `invalid`, `disposable`, `error` (the check itself failed; not charged). |
| `quality`     | `good`, `risky` or `bad`: the send / warm-up / drop decision.                                                                                                                                              |
| `subresult`   | Technical detail code: `no_mailbox`, `mailbox_full`, `dns_no_mx`, `dns_no_domain`, `syntax`, `greylisted`, `anti_spam_system`, ...                                                                         |
| `reason`      | The same in plain English.                                                                                                                                                                                 |
| `isFree`      | Free mailbox provider (Gmail, Outlook, Yahoo...).                                                                                                                                                          |
| `isRole`      | Role address (`info@`, `support@`...), usually poor for personal outreach.                                                                                                                                 |
| `didYouMean`  | Suggested correction for a likely typo.                                                                                                                                                                    |
| `duplicateOf` | Set when the same address appeared earlier in the input; it carries the first verdict and was not charged again.                                                                                           |
| `verifiedBy`  | `local` (format or DNS decided it) or `millionverifier`.                                                                                                                                                   |
| `error`       | Why a check failed when `result` is `error`.                                                                                                                                                               |

The `SUMMARY` record in the key-value store lists input rows, unique addresses, duplicates, counts by result and quality, how many were decided locally, how many were charged, warnings, and the Google Sheet outcome.

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

**$1 per 1,000 addresses verified**, no subscription, Apify platform usage included. One `email-verified` event is charged per unique address that received a verdict, including addresses decided locally as invalid. Not charged: duplicates, blank cells, text that is not an address, rows the check could not complete (`error`), and anything after your run's cost limit.

| List size         | Cost (no duplicates) | Actor fee with your own MillionVerifier key (plus your MillionVerifier credits) |
| ----------------- | -------------------- | ------------------------------------------------------------------------------- |
| 1,000 addresses   | $1.00                | $0.20                                                                           |
| 10,000 addresses  | $10.00               | $2.00                                                                           |
| 100,000 addresses | $100.00              | $20.00                                                                          |

A 10,000-row list with 15% duplicates costs about $8.50, because duplicates are free. If you also tick the Google Sheet output, the [Dataset to Google Sheets Sync](https://apify.com/osel_house/dataset-to-google-sheets) Actor charges its own fee on top: $0.002 per sync plus $0.00002 per row, about $0.20 for 10,000 rows.

If you already have a MillionVerifier account, paste your API key: the mailbox checks are then paid from your MillionVerifier credits and this Actor charges **$0.20 per 1,000** as a platform fee.

Set **Maximum cost per run** in the run options to cap spending; the run stops cleanly at the cap and tells you how many rows are left. To continue later, open the run and click **Resurrect**: it carries on from where it stopped and does not charge again for the addresses already verified.

### Tips

- **Chain it after a scraper**: use the scraper's dataset ID as input and keep `keepInputFields` on; the output is the scraped list plus the verdict.
- **Cold outreach**: send to `quality = good`. Treat `catch_all` as a separate warm-up segment; never bulk-send to `bad`.
- **Form clean-up**: `didYouMean` plus `isFree` finds the `gmial.com` typos worth fixing by hand.
- **Big lists**: the default run timeout is 24 hours, so a 100,000-address list finishes in one run even when many mail servers are slow. If a run still stops early (timeout, cost limit, vendor outage), **Resurrect** it instead of starting a new run: already verified addresses are not charged twice.
- **Slow corporate servers**: raise `verificationTimeoutSecs` to 30 if many results come back `unknown` with `timeout`.

### Other Actors by osel_house

- [Dataset to Google Sheets Sync](https://apify.com/osel_house/dataset-to-google-sheets): send any Actor's results to a Google Sheet on every run, no Google login (this Actor uses it for the sheet output).
- [Sitemap URL Extractor & Change Monitor](https://apify.com/osel_house/sitemap-extractor-monitor): every URL from a site's sitemaps, or only what changed since last time.
- [NWS Official Weather](https://apify.com/osel_house/nws-official-weather): official NOAA/NWS daily high, low and METAR observations as JSON.

### FAQ, privacy and support

**Is this legal / GDPR friendly?** Verifying addresses you lawfully hold is standard list hygiene. No email is sent to the addresses. Addresses are sent to MillionVerifier (Budapest, EU) for the mailbox check; they state uploaded data is deleted after 30 days. Nothing is stored outside your run's own dataset and key-value store, and your Google Sheet if you asked for one. You are responsible for having a lawful basis to hold and contact the addresses, and this Actor must not be used to build or validate lists for unsolicited mass mail.

**Why "catch-all"?** Some domains accept mail for any address and bounce later. No verifier can confirm those mailboxes; they are reported as risky, not invalid.

**Why "unknown"?** The mail server was slow, greylisted the check, or sits behind an anti-spam filter. The Actor retries once; running again later often resolves the rest.

**The run stopped early. Do I pay twice?** No. Resurrect the run (Console > the run > three-dot menu > Resurrect) and it continues from the last saved row. Addresses that already got a verdict are remembered and not charged again.

**Can I verify from Excel?** Export the sheet as CSV first, or paste the Google Sheet link.

**Something broke?** Open the **Issues** tab on this page; include the run ID. Custom pipelines (scrape, verify, enrich, deliver) are available on request.

# Actor input Schema

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

Paste addresses, one per line. You can also leave this empty and use a dataset or a file below. Duplicates are checked once.

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

A dataset that has an email column, for example the output of a LinkedIn or Google Maps lead scraper. Every other column is kept in the output next to the verdict.

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

Name of the field that holds the address in the dataset or file (nested paths like contact.email work). Leave empty to detect it automatically: the first column named like 'email', otherwise the column whose values look like addresses.

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

Upload a CSV or TXT file (one address per line, or a header row with an email column), paste a public file URL, or paste a Google Sheet link that is shared as 'Anyone with the link can view'. XLSX is not supported: export it as CSV first.

## `outputFilter` (type: `string`):

All = every address with its verdict. Valid only = addresses that are safe to send to. Valid and risky = drop only the invalid and disposable ones. Charging is per address verified, not per row output.

## `keepInputFields` (type: `boolean`):

When the addresses come from a dataset or a CSV with more columns (name, company, phone...), copy those columns into each output row. Off = output only the verification fields.

## `googleSheetUrl` (type: `string`):

Also write the results into this Google Sheet when the run finishes. Share the sheet as Editor with the service-account address shown in the README of our 'Dataset to Google Sheets Sync' Actor (its run log prints it too); that Actor's small per-row fee applies.

## `googleSheetTab` (type: `string`):

Which tab to write to. Created if missing.

## `googleSheetMode` (type: `string`):

Replace = clear the tab and write the fresh results. Append = add the new rows under what is there.

## `recheckUnknown` (type: `boolean`):

Mail servers sometimes answer 'try again later' (greylisting). On = addresses that came back 'unknown' are checked a second time after a short pause, which resolves many of them. This Actor does not charge extra for it.

## `verificationTimeoutSecs` (type: `integer`):

How long the mail server is given to answer before the address is reported 'unknown'. 2 to 60. Slow servers may need 30.

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

Higher is faster. 20 is a good default; the speed mostly depends on how quickly the mail servers answer.

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

Stop reading input after this many rows. Protects against an accidentally huge dataset. Up to 500,000 per run; above about 200,000 raise the run memory to 1 GB.

## `millionVerifierApiKey` (type: `string`):

If you already have a MillionVerifier account, paste its API key and the checks are paid from your credits; this Actor then charges only a small platform fee per address. Leave empty to use the built-in key.

## Actor input object example

```json
{
  "emails": [
    "hello@apify.com",
    "nobody-here-12345@apify.com",
    "not-an-email"
  ],
  "outputFilter": "all",
  "keepInputFields": true,
  "googleSheetTab": "Verified emails",
  "googleSheetMode": "replace",
  "recheckUnknown": true,
  "verificationTimeoutSecs": 15,
  "maxConcurrency": 20,
  "maxEmails": 100000
}
```

# Actor output Schema

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

No description

## `summary` (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": [
        "hello@apify.com",
        "nobody-here-12345@apify.com",
        "not-an-email"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("osel_house/email-list-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": [
        "hello@apify.com",
        "nobody-here-12345@apify.com",
        "not-an-email",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("osel_house/email-list-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": [
    "hello@apify.com",
    "nobody-here-12345@apify.com",
    "not-an-email"
  ]
}' |
apify call osel_house/email-list-verifier --silent --output-dataset

```

## MCP server setup

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