# Email Verifier — Bulk SMTP & MX Validation, Catch-all (`bovi/email-verifier`) Actor

Verify email addresses you provide with syntax, disposable, role, MX, and optional SMTP checks. Get status, score, and verification fields. Run in your Apify account using emails. Pricing: $0.0009 per dataset item + $0.00005 per Actor Start event on Free.

- **URL**: https://apify.com/bovi/email-verifier.md
- **Developed by:** [Vitalii Bondarev](https://apify.com/bovi) (community)
- **Categories:** Lead generation, Business
- **Features:** Limited permissions
- **Stats:** 48 total users, 26 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.87 / 1,000 email-verifications

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

## Email Verifier for Bulk Email Validation

Email Verifier checks the addresses you provide with syntax, disposable and role-account lists, DNS MX records, and an SMTP probe, and returns a status, score, and verification fields. Outbound port 25 is normally closed on Apify, so most runs rely on the non-SMTP checks (see Limits).

- Verify addresses in bulk without sending a message or issuing a `DATA` command
- Clean B2B lead lists and outreach databases with status, confidence scores, and MX details
- Validate signup addresses and flag disposable or generic role accounts
- Catch-all detection when an SMTP probe completes

### What you get

Each address goes through syntax, disposable-domain, role-account, DNS MX, optional SMTP, and catch-all checks.

| Field | Type | Description |
|---|---|---|
| `email` | string | Normalized, lowercased address; not null or empty in a returned record |
| `status` | string | `deliverable`, `undeliverable`, `risky`, or `unknown`; not null |
| `score` | integer | Confidence from 0 to 100; not null |
| `syntax_valid` | boolean | Passes syntax validation; not null |
| `is_disposable` | boolean | Domain is on the built-in temporary-mail hotlist; not null |
| `is_role` | boolean | Generic role local part such as `info`, `support`, or `admin`; not null |
| `mx_found` | boolean | Result of the DNS MX check; not null |
| `mx_host` | string | Highest-priority MX hostname; null when no MX is found |
| `smtp_code` | integer | Raw SMTP response; null if SMTP is unreachable, skipped, or does not complete |
| `smtp_message` | string | SMTP response text, or a reason such as the port-25-blocked note; null when SMTP is skipped |
| `is_catch_all` | boolean | Domain accepts arbitrary local parts; null when SMTP is skipped, unreachable, or catch-all status cannot be determined |
| `checked_at` | ISO 8601 string | UTC timestamp for the check; not null |

A field is `null` when the source does not provide the value.

Disposable domains are checked against a built-in hotlist of known temporary-mail providers. It is deliberately not exhaustive, so a disposable domain missing from it is not flagged. Generic role local parts such as `info`, `support` and `admin` are flagged in `is_role`.

If DNS returns no MX record, the Actor checks for an A-record fallback. A DNS timeout or other DNS failure counts as no MX (`undeliverable`, score 0).

SMTP connects to the highest-priority MX host on port 25 and uses `HELO`, `MAIL FROM`, and `RCPT TO`. It reads the `RCPT TO` response and closes the connection without delivering a message. After a `250` response, a random non-existent address is checked to identify catch-all domains.

Before the first address, the Actor tests whether port 25 is reachable. If it is closed, every address skips SMTP. If a run starts with port 25 open but 5 probes in a row are unreachable and none has connected, the Actor stops probing for the rest of the run.

### Sample output

A record from a real run on Apify for a public throwaway address on the disposable `mailinator.com` domain, where port 25 was blocked: `smtp_code` and `is_catch_all` are `null`, and `smtp_message` carries the blocked-port note. Score 5 is what a disposable domain gets when an MX is found and the SMTP probe is unreachable. A role address on a non-disposable domain in the same situation returns `unknown`, score 60 instead.

```json
{
  "email": "user@mailinator.com",
  "syntax_valid": true,
  "is_disposable": true,
  "is_role": false,
  "mx_found": true,
  "mx_host": "mail.mailinator.com",
  "smtp_code": null,
  "smtp_message": "connection_failed: outbound port 25 is blocked in this environment (detected once per run; SMTP probe skipped, MX/DNS result only)",
  "is_catch_all": null,
  "status": "undeliverable",
  "score": 5,
  "checked_at": "2026-10-30T01:29:09Z"
}
```

### Pricing

The Actor uses pay-per-event pricing: a start charge per run (one event per GB of run memory, minimum one) and a charge per dataset item.

You pay only for the events listed below. This Actor does not use the Pay per event + usage option, so the platform usage of your runs (compute, proxy traffic and storage) is not billed to you separately.

| Plan | Actor Start | Dataset item |
|---|---:|---:|
| Free | $0.00005 per event on Free plan | $0.0009 per event on Free plan |

The `email-verification` event name has no price of its own in the price list; each returned record is billed as the priced dataset item in the table above.

For one Actor Start event:

**1,000 dataset events: $0.00005 + (1,000 × $0.0009) = $0.90005 on the Free plan**

**10,000 dataset events: $0.00005 + (10,000 × $0.0009) = $9.00005 on the Free plan**

The Apify Free plan includes **$5 of monthly platform usage credit**; the $5 credit covers 5,555 priced dataset events after one Free-plan Actor Start event.

### How to use

1. **Prepare** an `emails` list of plain strings or objects with an `email` key.
2. **Choose** concurrency, SMTP timeout, and SMTP mode. Use `skipSmtp: true` to skip the SMTP step and avoid waiting on a closed port.
3. **Run** the Actor with an input such as the one below (start it with Start in Apify Console, or through the Apify API; the Console prefill already holds three runnable addresses):

```json
{
  "emails": [
    "user@mailinator.com"
  ],
  "concurrency": 10,
  "smtpTimeoutSecs": 10,
  "skipSmtp": false
}
```

4. **Inspect** returned records for status, score, flags, and MX/SMTP values.
5. **Filter** records according to your workflow, reviewing `risky`, disposable, and role-account results separately.
6. **Schedule** the run through Apify Schedules as a starting point, such as weekly for lead-list verification.

Export the dataset as JSON or CSV, or read it through the Apify dataset API.

### Input parameters

| Parameter | What it does | Default |
|---|---|---|
| `emails` | Required plain strings or objects with an `email` key; maximum 10,000 entries per run | Required |
| `concurrency` | Emails verified in parallel; range 1–50, with 5–20 recommended | `10` |
| `smtpTimeoutSecs` | Timeout for each SMTP attempt; range 3–30 seconds | `10` |
| `skipSmtp` | Skips SMTP and uses MX-only mode | `false` |
| `disableUsageStats` | Set to `true` to turn off the run summary described in Usage statistics. | `false` |

### Limits and edge cases

Output rows reach the dataset as each address finishes, before its charge is attempted. If a run is aborted, times out, or fails after rows are pushed, those rows remain and addresses not yet pushed have no rows.

The Actor writes an `OUTPUT` record with `emails_verified` and a reason to the run's default key-value store when it:

- fails on unusable input;
- stops early on the cost-efficiency check;
- finishes with port 25 blocked.

A run you abort yourself, or one that times out, does not write it.

| Condition | What you see | What to do |
|---|---|---|
| More than 10,000 entries | The run uses the first 10,000 | Split the list |
| `emails` is empty, or has no usable strings or `email` values | The run fails with an input-needed message and `emails_verified: 0` | Add addresses as strings or objects with an `email` key |
| Syntax is invalid | `undeliverable`, score 0; no network round-trip | Correct the address |
| No MX record, no A-record fallback, or a DNS timeout | `undeliverable`, score 0 | Check the domain; re-run if DNS was flaky |
| Port 25 is blocked or SMTP times out (the usual case on Apify) | `smtp_code` is null. Known providers (Gmail, Outlook, Yahoo, iCloud, Proton, AOL, Zoho and similar) return `deliverable`, score 80, a domain-name heuristic with no mailbox check. Domains on the disposable hotlist return `undeliverable`, score 5. Other domains return `unknown`, score 60 | Treat 80 and 60 as unconfirmed |
| `skipSmtp` is `true` | No SMTP probe; the same MX-only rules apply | Use it to avoid the port-25 preflight |
| SMTP returns `250` and the random address is rejected | `deliverable`, score 100 | Highest-confidence result |
| SMTP returns `250` but the catch-all check does not complete | `deliverable`, score 100, `is_catch_all` null | Catch-all status is unknown; review separately |
| SMTP returns `250` for the random address too | `risky`, `is_catch_all: true`, score 40 | Review the mailbox separately |
| SMTP returns a 4xx response | `risky`, score 35 | Re-verify later or review it |
| SMTP returns a 5xx response | `undeliverable`, score 0 | Remove or correct the address |
| SMTP returns any other code | `unknown`, score 50 | Re-verify or review it |
| A server accepts an address with `250` | Acceptance does not guarantee that a later message will not bounce silently | Treat the score as a verification signal |
| High concurrency is used against one domain | Mail servers may return more 4xx responses from rate-limiting | Reduce `concurrency`; the default of 10 is conservative |
| The cost-efficiency stop occurs before the first row | The run fails with `emails_verified: 0` | Run again with a smaller list |
| The cost-efficiency stop occurs after rows are written | A partial run reports `emails_verified` and `emails_not_processed` | Run the remaining addresses later with a smaller list |

To see whether SMTP worked, check `smtp_code`: `null` means the SMTP step did not complete.

### FAQ

#### How much does it cost to scrape email addresses?

This Actor verifies addresses rather than finding them. Actor Start costs $0.00005 per event on the Free plan and each dataset item costs $0.0009 per event on the Free plan; see Pricing for the worked calculation.

#### Why can a result be unknown or risky?

`unknown` generally means an MX result was found but SMTP could not be reached (score 60), or an unexpected SMTP code came back (score 50). `risky` means the domain is catch-all or the SMTP server returned a temporary 4xx response.

#### What happens if the run returns no results or does not work?

An empty `emails` list or a list without usable entries fails with an input-needed message. If SMTP is unavailable, the Actor still returns records from the other checks.

#### Is it legal to verify email addresses using public data?

The Actor works with the addresses you provide, DNS records and mail-server responses. Whether verifying them is lawful depends on your use case and local law, so check that before use.

### Related actors

- [Bulk Email Verifier](https://apify.com/bovi/bulk-email-verifier) — validates emails with MX, disposable, and role checks.
- [Email Finder](https://apify.com/bovi/email-finder) — finds a work email from a name and domain before verification.
- [Email Finder Bulk](https://apify.com/bovi/email-finder-bulk) — finds emails from lists of domains or people before verification.

### Usage statistics

This Actor creates a small, content-free summary at the end of each run, used only to monitor reliability and improve this Actor. No input values, credentials or scraped data are sent.

A copy of the exact record for your run is saved as `USAGE_STATS` in your own Apify key-value store.

To opt out, set `disableUsageStats` to `true` in the input. Nothing is sent then.

[Full list of collected fields](https://api.apify.com/v2/key-value-stores/v9rgHjMKLDGtHf8tD/records/index.html)

# Actor input Schema

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

List of email addresses to verify. Each entry is either a plain email string (e.g. 'user@example.com') or an object with an 'email' key. Max 10,000 per run.

## `concurrency` (type: `integer`):

Number of emails to verify in parallel. Higher = faster but may trigger rate-limits on target mail servers. Recommended: 5–20.

## `smtpTimeoutSecs` (type: `integer`):

Timeout for each SMTP connection attempt. Increase if many unknown results on slow mail servers.

## `skipSmtp` (type: `boolean`):

Skip the SMTP verification step and rely on MX lookup only. Faster but less accurate — use when running from a network that blocks outbound port 25.

## `disableUsageStats` (type: `boolean`):

See the README Usage statistics section for details.

## Actor input object example

```json
{
  "emails": [
    "test@gmail.com",
    "info@example.com",
    "user@mailinator.com"
  ],
  "concurrency": 10,
  "smtpTimeoutSecs": 10,
  "skipSmtp": false,
  "disableUsageStats": false
}
```

# Actor output Schema

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

Dataset containing Email Verifier records (email, status, score, syntax_valid, is_disposable, is_role, mx_found, mx_host, smtp_code, is_catch_all, checked_at).

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

// Run the Actor and wait for it to finish
const run = await client.actor("bovi/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": [
        "test@gmail.com",
        "info@example.com",
        "user@mailinator.com",
    ] }

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

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "emails": [
    "test@gmail.com",
    "info@example.com",
    "user@mailinator.com"
  ]
}' |
apify call bovi/email-verifier --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Zbaas8lOx7qQ1Nazc/builds/I6L1V5zizJoXy5Jcl/openapi.json
