# Email Verifier (`dami_studio/email-verification`) Actor

Email verifier: bulk email verification before you send. Paste a list, or read a dataset, CSV, Excel or Google Sheets file. Each address is checked for syntax and a working MX record, and flagged if disposable, a role inbox like info@ or a free provider. It never sends mail. $0.49 per 1,000.

- **URL**: https://apify.com/dami_studio/email-verification.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Lead generation, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 170 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.48 / 1,000 emails

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

**Verify a bulk email list before you send: syntax, MX records, disposable and role checks**: paste
a list of addresses, pick a dataset from an earlier run, or hand it a CSV, Excel or Google Sheets
file, and each address comes back as a row with a status, a 0 to 100 score, and the reason behind
it. Whether the domain can actually receive mail, whether it is a throwaway, whether
the address is a shared inbox like `info@`, and whether the domain is a free consumer provider.

Here is the part that decides whether this is the right tool. **It never sends mail, and from
Apify's cloud it cannot ask the mail server about a mailbox either, so it cannot prove a mailbox
exists.** `valid` here means the syntax passed
and the domain accepts mail, which is the check you run before a send so bounces do not wreck your
sender reputation. It is not a deliverability guarantee.

| | |
|---|---|
| **Input** | A list of email addresses, a dataset from another run, or a CSV, Excel or Google Sheets file |
| **Output** | One row per address: status, score, reason, MX records, and disposable, role and free-provider flags |
| **Ceiling** | No fixed size for a pasted list; a dataset or a file adds up to 100,000 addresses. The run's own time limit, 5 minutes by default, is the real one |
| **Account needed** | None, no API key and no mailbox |
| **Price** | $0.49 per 1,000 addresses checked on the Free plan, a little less on paid plans. The free plan's $5 a month covers about 10,200 |

### 🔍 What Email Verifier does

Each address goes through the same passes. The syntax is checked. The domain is looked up in public
DNS for MX records. The domain is matched against a built-in list of disposable providers, the local
part against a list of role names, and the domain against the free consumer providers.

A domain that can receive mail says so in public DNS with an MX record, and a domain that has sworn
off mail says that too, with a null MX. Both answers are useful, and both are in the row.

Duplicate addresses are lowercased and collapsed before anything is checked, so the same address in
your list twice is one row and one charge.

There is also a bounded SMTP probe you can switch on. Read the limits before you rely on it.

### 📋 What data you get from each email address

| What you get | Field |
|---|---|
| The verdict and a 0 to 100 score behind it | `status`, `confidence` |
| The reason for the verdict, in one word | `reason` |
| Whether the address is written correctly | `syntaxValid` |
| Whether the domain has mail servers, and the list of them | `mxFound`, `mxRecords` |
| Throwaway domain, shared inbox, free consumer provider | `isDisposable`, `isRoleAccount`, `isFreeProvider` |
| Whether a mail server confirmed this exact recipient | `mailboxVerified` |
| The SMTP probe's answer, when you switch it on | `smtp`, `smtpNote` |
| The address as checked, lowercased, and its domain | `email`, `domain` |

### ▶️ How to verify an email list before you send

1. Open [Email Verifier](https://apify.com/dami_studio/email-verification) and click
   **Try for free**.
2. Paste your addresses into **Email addresses**, one per line. Or pick a dataset under
   **Addresses from a dataset**, or upload a file under **Addresses from a CSV or Excel file**.
3. Leave **Attempt bounded SMTP probe** off for a first run.
4. Click **Start**.
5. Download the dataset as JSON, CSV or Excel, or read it from the Apify API.

Start it with the list empty and you get one free sample row showing the output shape.

### 💰 How much does it cost to verify email addresses?

**$0.49 per 1,000 addresses checked** on the Free plan. Paid plans pay a little less: $0.485 on
Starter, $0.48 on Scale and $0.475 on Business. On the free plan, the $5 Apify gives you each month
covers about 10,200 addresses.

One charge per completed check. The sample row, per-address failure rows and diagnostic rows are not
billed, and a duplicate address collapsed into one row is billed once. An address that fails on
syntax still gets its row, free. One case is billed although it tells you little: an `unknown` row
where DNS gave no answer.

### 📥 What you give it

```json
{
  "emails": ["person@example.com", "info@example.com", "bad@@example.com"],
  "checkSmtp": false,
  "smtpTimeoutMs": 5000,
  "maxConcurrency": 10
}
```

| Field | Default | What it is |
|---|---|---|
| `emails` | none | The list. The form starts with two example addresses in it. Duplicates are collapsed. Leave it empty and you get one free sample row showing the output shape. |
| `checkSmtp` | `false` | Try a short SMTP probe against the domain's first mail host. It never sends a message body. See the limits for what it can and cannot tell you. |
| `smtpTimeoutMs` | `5000` | Deadline per connection, capped at 8000, so one dead mail host cannot hold up a run. |
| `maxConcurrency` | `10` | How many addresses are checked at once, 1 to 25. Lower it if you want gentler DNS traffic. |
| `inputDatasetId` | none | A dataset from an earlier run. It is read a page at a time and its addresses join the list. |
| `fileUrl` | none | A CSV or .xlsx file, uploaded or linked, or a Google Sheets link shared as "Anyone with the link can view". First sheet only, 5 MB at most. |
| `emailField` | none | The dataset field (`email`, or `contact.email` for a nested one) or the file's column heading. Empty means the fields or columns with "mail" in their name; a file without one is searched cell by cell. |
| `maxItems` | none | Stop after this many different addresses, across all three sources. Empty means the whole pasted list, plus up to 100,000 from a dataset or a file. |

Checking another run's results looks like this:

```json
{
  "inputDatasetId": "your-dataset-id",
  "emailField": "email",
  "maxItems": 5000
}
```

### 📤 What you get back

A real row from a recent run. `example.com` publishes a null MX, which is a domain stating in public
that it accepts no mail, so the address is `invalid` with high confidence:

```json
{
  "ok": true,
  "email": "person@example.com",
  "status": "invalid",
  "confidence": 98,
  "mailboxVerified": false,
  "syntaxValid": true,
  "domain": "example.com",
  "mxFound": false,
  "mxRecords": [],
  "isDisposable": false,
  "isRoleAccount": false,
  "isFreeProvider": false,
  "smtp": null,
  "smtpNote": null,
  "reason": "no_mx_record"
}
```

| Field | How to read it |
|---|---|
| `status` | `valid`, `risky`, `invalid` or `unknown`. |
| `mailboxVerified` | `true` only when a mail server accepted this exact recipient, `false` when one permanently refused it or the domain cannot receive mail at all, `null` when nobody could ask. |
| `reason` | `mx_valid`, `no_mx_record`, `invalid_syntax`, `disposable_domain`, `role_account`, `smtp_recipient_accepted`, `smtp_recipient_rejected`, `dns_inconclusive` or `sample_only`. |
| `mxRecords` | The mail servers as `{priority, exchange}`, sorted by priority. `mxFound` is `null` when DNS itself gave no answer. |
| `smtp` | `null` unless you switch the probe on. Otherwise its status, code, stage and reason. |

### 🧾 Reading the output

| Row | How to spot it | Charged |
|---|---|---|
| A completed check | `ok: true` and a `status` | yes |
| An address with broken syntax | `status: "invalid"` and `reason: "invalid_syntax"` | no |
| The sample row | `_sample: true` and `reason: "sample_only"` | no |
| A per-address failure | `ok: false` with an error | no |
| A diagnostic | `errorCode` of `NO_RESULTS` or `INTERNAL_ERROR`. An `INTERNAL_ERROR` before any email was checked is the one case that ends the run as failed | no |
| A source that could not be read | `errorCode` of `BAD_INPUT`, `SOURCE_ERROR` or `NO_ADDRESSES`, with `source` set to `dataset` or `file` and the reason in `error`. If nothing else was given, nothing is checked and the status message says why | no |
| A time-limit notice | `errorCode` of `TIME_LIMIT`: the run got close to its time limit and stopped on its own. The emails checked before that are delivered and charged, the rest have no row and no charge. Give the run more time, or send fewer emails. The run still ends as succeeded, with an amber status message | no |

What each status means:

| Status | Meaning |
|---|---|
| `valid` | Syntax passed and the domain accepts mail. Keep it in the list. |
| `risky` | The domain is fine but something about the address is not. A shared inbox or a disposable provider. |
| `invalid` | Bad syntax, or the domain publishes no MX and says it takes no mail. |
| `unknown` | The check ran but DNS gave no usable answer, so `reason` is `dns_inconclusive`. This is a completed check and it is billed like any other. |

The Console's **overview** table leaves out `reason`, `mailboxVerified` and `isFreeProvider`, which
is exactly what you need to tell one `unknown` from another. Read the JSON for those.

### 💡 What people use it for

- Cleaning a list before a send, so hard bounces do not damage the sending domain.
- Splitting a signup list into personal and shared inboxes, using `isRoleAccount`.
- Dropping throwaway signups, using `isDisposable`.

From a list of company websites to a checked contact sheet, in three runs:

1. Run [Contact Details Scraper](https://apify.com/dami_studio/contact-details-scraper) on the
   websites.
2. Paste the addresses from its `emails` lists into **Email addresses** here and keep the `valid`
   rows.
3. Paste the `e164` numbers from its `phones` lists into **Phone numbers** on
   [Phone Number Validator](https://apify.com/dami_studio/phone-number-validator).

### 🚧 What it does not do

- **It never sends mail**, and without a mail server to ask it cannot confirm a mailbox exists or
  predict a bounce.
- **The SMTP probe does not connect from here.** Apify's runners block outbound port 25, measured
  across eight mail hosts, so in the cloud `mailboxVerified` comes back `null` on every row. Run the
  actor on your own machine and the probe behaves normally. Big providers refuse to confirm or deny
  a recipient anyway.
- **Catch-all domains look the same as working mailboxes.** Telling them apart means asking the mail
  server, which the cloud probe cannot reach.
- **The disposable list is built in and holds 16 domains.** It catches the common services, not
  every throwaway on the internet.
- **A run stops at its time limit**, 5 minutes by default. Addresses checked by then are delivered
  and charged; the rest get no row and no charge. For a long list, raise the limit, or set
  **Maximum addresses** and run the rest later.
- **Old .xls files, password-protected sheets and other sheets than the first** are not read.
- **25 concurrent checks is the ceiling.**
- **It is a list-cleaning signal, not consent.** Only run addresses you are allowed to process.
- It is not an official tool of any platform or company named on this page. Dami's Studio is independent and is not affiliated with or endorsed by any of them.

### 🧭 Which lead tool do you need?

| If you want | Use |
|---|---|
| To clean a list of addresses you already have | This one |
| Emails, phones and socials pulled off a list of websites | [Contact Details Scraper](https://apify.com/dami_studio/contact-details-scraper) |
| The same kind of check for phone numbers | [Phone Number Validator](https://apify.com/dami_studio/phone-number-validator) |
| Businesses with phone, website and address | [Google Maps Scraper](https://apify.com/dami_studio/google-maps-scraper) |

### ❓ Questions people ask

#### Does it prove the person's inbox exists?

No. From Apify's cloud it cannot reach the mail server to ask, so `valid` means the domain accepts
mail and the address is worth keeping.

#### Why is mailboxVerified always null?

Outbound port 25 is blocked on Apify's runners, so the probe never connects in the cloud.

#### Why is a good address marked risky?

Usually because it is a role account. `info@` and `sales@` are real addresses, but a team shares them
and mail sent there often goes unread.

#### Am I billed for an address with a typo in it?

No. A malformed address still gets its row, marked `invalid_syntax`, and is not charged. Duplicates
are collapsed before anything is checked and billed once.

#### Can I call it from code or connect it to an AI assistant?

The [API tab](https://apify.com/dami_studio/email-verification/api/python) has code for Python, JavaScript and the
CLI. MCP clients like Claude and ChatGPT use
`https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/email-verification`; coding assistants can load [our
skill file](https://api.apify.com/v2/key-value-stores/BTBHU3DdiuoF4JOsK/records/apify-web-checks-monitors-SKILL.md).
All three bill your Apify account at the same price.

#### Is verifying email addresses legal?

Verifying addresses you hold is ordinary list hygiene, but an email address is personal data and
data-protection law covers it, so have a reason for holding the list. Apify's
[write-up on scraping and the law](https://blog.apify.com/is-web-scraping-legal/) is a good start,
and we are not lawyers.

### 🆘 If something breaks

Open the **Issues** tab on the actor page. Send the run ID and one address that behaved oddly. The
`reason` field usually explains the verdict on its own.

# Actor input Schema

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

Email addresses to verify. Duplicate addresses are normalized and processed once. Leave empty to run the free sample row.

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

Optional. Pick the dataset of an earlier run, for example the results of another actor, and the addresses in it are checked as well. It is read a page at a time. Name the field that holds the addresses below, or leave that empty to use the fields with "mail" in their name.

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

Optional. Upload a CSV or Excel (.xlsx) file, or paste a link to one, or a Google Sheets link. A Google Sheet must be shared as "Anyone with the link can view". Only the first sheet of an Excel file is read, and files over 5 MB are refused.

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

Optional. The dataset field (such as email, or contact.email for one inside another) or the file's column heading that holds the addresses. Leave empty to use the fields or columns with "mail" in their name; a file with no such column is searched cell by cell.

## `maxItems` (type: `integer`):

Optional. Stop after this many different addresses, counted across the list, the dataset and the file. Leave empty to check the whole pasted list; a dataset or a file adds at most 100,000 addresses to a run either way.

## `checkSmtp` (type: `boolean`):

Attempt a short SMTP EHLO/MAIL FROM/RCPT TO probe against the first MX host. SMTP is best-effort: many providers block enumeration and Apify cloud may block outbound port 25, so inconclusive is reported rather than treated as invalid.

## `smtpTimeoutMs` (type: `integer`):

Per-connection SMTP deadline. It is capped at 8000 ms to keep cloud runs bounded.

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

Maximum number of email checks running at once. Lower this for conservative DNS and SMTP traffic.

## Actor input object example

```json
{
  "emails": [
    "person@example.com",
    "info@example.com"
  ],
  "checkSmtp": false,
  "smtpTimeoutMs": 5000,
  "maxConcurrency": 10
}
```

# Actor output Schema

## `dataset` (type: `string`):

One dataset row per completed verification. Empty input returns one labeled, uncharged sample row.

# 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": [
        "person@example.com",
        "info@example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/email-verification").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": [
        "person@example.com",
        "info@example.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/email-verification").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": [
    "person@example.com",
    "info@example.com"
  ]
}' |
apify call dami_studio/email-verification --silent --output-dataset

```

## MCP server setup

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

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/4hhF0MPOsSvhvmRZf/builds/39ITJ9MEYI7EFgM2p/openapi.json
