# Bulk Email Verifier (syntax, MX, disposable, catch-all, SMTP) (`rod_analytics/bulk-email-verifier`) Actor

Verify email lists in bulk: RFC syntax, typo suggestions, MX records, disposable and free provider detection, role accounts, catch-all detection and SMTP mailbox check without sending mail.

- **URL**: https://apify.com/rod\_analytics/bulk-email-verifier.md
- **Developed by:** [Rod Services](https://apify.com/rod_analytics) (community)
- **Categories:** Lead generation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 results

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

### What does Bulk Email Verifier do?

**Bulk Email Verifier** checks email lists in bulk and tells you which addresses are **valid, invalid, risky or unknown**. It runs RFC syntax validation, **typo suggestions** (gmial.com to gmail.com), **domain and MX record lookups**, **disposable email detection**, free provider and **role account** flags. With your own SOCKS5 proxy it also runs a **catch-all check** and an **SMTP mailbox check** that talks to the mail server without sending any email.

> **What `valid` means.** Without the SMTP check, `valid` means the address is well formed and its domain can receive mail (`verificationLevel: "dns"`). **The mailbox itself is not confirmed.** Only rows with `verificationLevel: "smtp"` were confirmed by the recipient mail server. SMTP needs your own SOCKS5 proxy, because Apify blocks port 25.

Paste addresses or give a CSV URL, click Start, and download clean results as JSON, CSV or Excel. As an Apify Actor you also get an API, scheduling, webhooks, integrations with Make, Zapier and Google Sheets, and run monitoring.

### Why use Bulk Email Verifier?

- **Protect sender reputation.** Remove hard bounces before a campaign. High bounce rates get domains blocked.
- **Clean signup forms and CRMs.** Catch typos like `john@gmail.con` and throwaway inboxes from Mailinator or 10MinuteMail.
- **Keep B2B lists useful.** Flag role accounts such as `info@` or `sales@` that rarely reach a decision maker.
- **Cheap and fast.** 1,000 addresses finish in about 4 seconds in DNS mode. Domains are looked up once and cached.
- **No lock-in.** Plain dataset output, API access, no monthly plan.

### How to verify an email list

1. Open the **Input** tab.
2. Paste addresses into **Email addresses**, or put a CSV or text file link into **CSV or text file URL**.
3. Leave **SMTP mailbox check** off unless you have a SOCKS5 proxy. See below.
4. Click **Start**. The prefilled 5 address example finishes in a few seconds.
5. Open the **Output** tab. Filter by `status` or download the dataset.

### Input

All fields are in the Input tab. JSON example:

```json
{
    "emails": ["info@apify.com", "example.user@gmial.com", "test@mailinator.com"],
    "emailsFileUrl": "https://example.com/contacts.csv",
    "smtpCheck": false,
    "socksProxyUrl": "socks5://user:pass@proxy.example.com:1080",
    "heloHost": "verify.yourdomain.com",
    "fromAddress": "bounce@yourdomain.com",
    "checkCatchAll": true,
    "timeoutSecs": 10,
    "maxConcurrency": 10,
    "maxConnectionsPerMx": 2,
    "greylistRetrySecs": 60
}
```

| Field                     | Meaning                                                                                                |
| ------------------------- | ------------------------------------------------------------------------------------------------------ |
| `emails`                  | Addresses to verify. Blocks separated by new lines, commas or semicolons also work.                    |
| `emailsFileUrl`           | CSV or text file. Every cell with an `@` is used, so headers and other columns are ignored. Max 50 MB. |
| `smtpCheck`               | Turn on the SMTP mailbox and catch-all check. Needs `socksProxyUrl` on Apify.                          |
| `socksProxyUrl`           | Your SOCKS5 proxy that can reach port 25. Stored encrypted.                                            |
| `heloHost`, `fromAddress` | Identity used in EHLO and MAIL FROM. Use a domain you control.                                         |
| `checkCatchAll`           | One extra RCPT per domain with a random address.                                                       |
| `greylistRetrySecs`       | Wait time before one retry after 4xx answers. 0 disables it.                                           |
| `timeoutSecs`             | Timeout per DNS query and per SMTP step.                                                               |
| `maxConcurrency`          | Domains processed in parallel.                                                                         |
| `maxConnectionsPerMx`     | Parallel SMTP sessions to one mail server.                                                             |

Duplicates are verified and billed once. Addresses count as duplicates after trimming, removing `mailto:` or `Name <...>` wrappers and lowercasing, so `INFO@Example.com` and `info@example.com` are one result. Plus tags are kept: `jane+news@gmail.com` and `jane@gmail.com` are two results.

### Output

One item per unique address. You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

```json
{
    "email": "example.user@gmial.com",
    "normalized": "example.user@gmial.com",
    "domain": "gmial.com",
    "status": "risky",
    "reason": "possible_typo",
    "verificationLevel": "dns",
    "isSyntaxValid": true,
    "hasMx": false,
    "mxHosts": ["gmial.com"],
    "isDisposable": true,
    "isFreeProvider": false,
    "isRole": false,
    "isCatchAll": null,
    "smtpChecked": false,
    "smtpCode": null,
    "smtpMessage": null,
    "didYouMean": "example.user@gmail.com",
    "checkedAt": "2026-09-27T10:32:25.005Z"
}
```

A run summary with counts per status and per verification level is saved as `SUMMARY` in the key-value store. It also shows `smtpMode`: `off`, `socks`, `direct` or `unavailable`.

### Data fields

| Field                     | Description                                                                                                                                |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `email`                   | Address as given                                                                                                                           |
| `normalized`              | Trimmed, lowercased, punycode domain                                                                                                       |
| `status`                  | `valid`, `invalid`, `risky` or `unknown`                                                                                                   |
| `reason`                  | Why. See the table below                                                                                                                   |
| `verificationLevel`       | `dns`: domain accepts mail, mailbox **not** confirmed. `smtp`: mail server answered for this mailbox. `syntax`: rejected before any lookup |
| `isSyntaxValid`           | Passes RFC 5321/5322 addr-spec rules                                                                                                       |
| `hasMx`                   | Domain has MX records                                                                                                                      |
| `mxHosts`                 | Mail hosts by priority. Domain itself when only an A record exists                                                                         |
| `isDisposable`            | Domain is on the disposable list                                                                                                           |
| `isFreeProvider`          | Gmail, Yahoo, Outlook, GMX and about 110 more                                                                                              |
| `isRole`                  | `info@`, `admin@`, `support@`, `noreply@` and similar                                                                                      |
| `isCatchAll`              | Server accepts any address. `null` when not tested                                                                                         |
| `smtpChecked`             | Mail server gave a definitive answer for this mailbox                                                                                      |
| `smtpCode`, `smtpMessage` | Raw RCPT TO reply                                                                                                                          |
| `didYouMean`              | Suggested fix for a likely domain typo                                                                                                     |
| `checkedAt`               | ISO timestamp                                                                                                                              |

#### Status and reason

| Status    | Reasons                                                                                             | What to do                    |
| --------- | --------------------------------------------------------------------------------------------------- | ----------------------------- |
| `valid`   | `mailbox_exists`: mailbox confirmed by SMTP, `verificationLevel` `smtp`                             | Safe to keep                  |
| `valid`   | `mx_found`, `a_record_only`: domain accepts mail, mailbox not checked, `verificationLevel` `dns`    | Keep, but expect some bounces |
| `invalid` | `invalid_syntax`, `domain_not_found`, `null_mx`, `no_mail_server`, `mailbox_not_found`              | Remove                        |
| `risky`   | `disposable`, `catch_all`, `possible_typo`, `mailbox_full`                                          | Review or remove              |
| `unknown` | `dns_error`, `smtp_blocked`, `greylisted`, `smtp_temporary_error`, `smtp_unreachable`, `smtp_error` | Retry later                   |

Without the SMTP check, `valid` means syntax, domain and mail server are fine and the domain is not disposable. **The mailbox itself is not confirmed**, so an address like `nobody-12345@gmail.com` is still `valid` at DNS level. Filter on `verificationLevel = smtp` when you need confirmed mailboxes. `a_record_only` means the domain has no MX record and mail would go to its A record, per RFC 5321. Many such domains do not accept mail in practice.

### How much does email verification cost?

Pay per event, no subscription:

- **$0.30 per 1,000 addresses** for syntax, DNS, MX, disposable, role and typo checks.
- **+$0.40 per 1,000** only for addresses where the SMTP check got a definitive answer. That is **$0.70 per 1,000** in total.
- A small start fee of $0.0005 per run, per GB of memory. The default 256 MB run pays it once.

Blocked, greylisted or unreachable SMTP answers are not charged as SMTP checks. The Apify free plan monthly credit covers tens of thousands of DNS level checks. Set **Maximum cost per run** in run options to cap spend. The Actor stops when it is reached. Addresses past the cap are not written and not charged.

### SMTP mailbox check and port 25

The mailbox check needs outbound TCP port 25. **Apify blocks port 25**, like most cloud providers. The Apify Proxy does not tunnel it either. So on Apify the SMTP step needs your own SOCKS5 proxy:

1. Rent a small VPS where port 25 is open. Many hosts open it on request.
2. Set reverse DNS of its IP to a hostname you own, e.g. `verify.yourdomain.com`.
3. Run a SOCKS5 server such as Dante with username and password.
4. Fill `socksProxyUrl`, `heloHost` and `fromAddress`, then turn on `smtpCheck`.

The Actor tests port 25 before it starts. If it fails, you get a warning, the run continues at DNS level, and no SMTP events are charged. The run summary shows `smtpMode`.

The SMTP step sends only `EHLO`, `MAIL FROM`, `RCPT TO` and `QUIT`. It **never sends DATA**, so no email is delivered.

### Tips and advanced options

- **Rate limits.** Keep `maxConnectionsPerMx` at 1 or 2. Domains on Google Workspace or Microsoft 365 share MX hosts, and the limit applies per host.
- **Greylisting.** Some servers answer 450 or 451 on first contact. The Actor waits `greylistRetrySecs` and retries once with the same probe addresses.
- **Big providers.** Yahoo and some Microsoft servers accept every RCPT or block probes. Expect `catch_all` or `smtp_blocked` there.
- **Speed.** DNS mode uses at least 20 parallel domains. 256 MB memory is enough for lists of 100,000+ addresses.

### Acceptable use

- **Only verify lists you have a lawful basis to process**, for example your own customers or signups under GDPR or similar law. You are the data controller for the addresses you upload.
- **No spam.** Do not use this Actor to build or clean lists for unsolicited email, or to guess addresses for harvesting.
- Respect mail server limits. Do not raise concurrency to hammer servers.
- The Actor stores results only in your own Apify storage. Delete datasets you no longer need.

### FAQ and support

**Is the result 100% accurate?** No verifier is. Catch-all servers, greylisting and anti-probe policies limit what SMTP can prove. `risky` and `unknown` exist for that reason.

**Where does the disposable list come from?** A pinned snapshot of [disposable-email-domains](https://github.com/disposable-email-domains/disposable-email-domains), CC0 licensed, 9,185 domains, commit `0655284`. Details in `data/disposable-source.json`.

**Does it send emails?** No. There is no DATA command.

**What happens if my input is invalid?** The run still ends **SUCCEEDED** and you pay only the small start fee. No address is charged and the dataset stays empty. The run status message explains the problem and the fix, and the same help record is saved in the key-value store under `INPUT_ERROR`:

```json
{
    "error": true,
    "errorCode": "EMPTY_INPUT",
    "message": "No email addresses in input.",
    "howToFix": "Add addresses to \"emails\" ..."
}
```

Codes: `EMPTY_INPUT` (no addresses), `NO_EMAILS_FOUND` (the file has no address), `FILE_UNREADABLE` (`emailsFileUrl` cannot be downloaded) and `INVALID_INPUT` (a wrong `socksProxyUrl`). Addresses with bad syntax are not an input error: they are verified and returned with status `invalid`.

Found a bug or need a feature? Open the **Issues** tab. Custom verification pipelines are available on request.

# Actor input Schema

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

Addresses to verify, one per item. You can also paste a block of addresses separated by new lines, commas or semicolons. Duplicates are verified once.

## `emailsFileUrl` (type: `string`):

Optional public URL of a CSV or plain text file. Every cell that contains an @ is treated as an address, so header rows and other columns are ignored. Max 50 MB.

## `smtpCheck` (type: `boolean`):

Ask the recipient mail server whether the mailbox exists (EHLO, MAIL FROM, RCPT TO, QUIT). No email is sent. Apify blocks outbound port 25, so this needs a SOCKS5 proxy below. Without it the Actor detects the block and continues with DNS level checks only, with no SMTP charges.

## `socksProxyUrl` (type: `string`):

socks5://user:password@host:port. The proxy host must allow outbound TCP port 25. Stored encrypted.

## `heloHost` (type: `string`):

Hostname sent in EHLO. Use a domain you control that matches the reverse DNS of the proxy IP. Defaults to the domain of the sender address.

## `fromAddress` (type: `string`):

Envelope sender used in MAIL FROM. Use an address on a domain you control. Defaults to verify@<HELO hostname>.

## `checkCatchAll` (type: `boolean`):

Send one extra RCPT for a random address per domain. If the server accepts it, the domain accepts everything and results become risky.

## `smtpPort` (type: `integer`):

Port on the recipient MX. Keep 25 unless you know what you are doing.

## `greylistRetrySecs` (type: `integer`):

When a server answers with a temporary 4xx code, wait this long and try those addresses once more. 0 disables the retry.

## `maxConnectionsPerMx` (type: `integer`):

Parallel SMTP sessions to one MX host. Keep it low to avoid rate limits and blocks.

## `timeoutSecs` (type: `integer`):

Timeout for each DNS query and each SMTP step.

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

How many domains are processed in parallel. Each domain uses at most one SMTP session at a time. DNS only runs use at least 20.

## Actor input object example

```json
{
  "emails": [
    "info@apify.com",
    "example.user@gmial.com",
    "test@mailinator.com",
    "hello@no-such-domain-4f7a2c.com",
    "not-an-email@"
  ],
  "smtpCheck": false,
  "checkCatchAll": true,
  "smtpPort": 25,
  "greylistRetrySecs": 60,
  "maxConnectionsPerMx": 2,
  "timeoutSecs": 10,
  "maxConcurrency": 10
}
```

# 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": [
        "info@apify.com",
        "example.user@gmial.com",
        "test@mailinator.com",
        "hello@no-such-domain-4f7a2c.com",
        "not-an-email@"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("rod_analytics/bulk-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": [
        "info@apify.com",
        "example.user@gmial.com",
        "test@mailinator.com",
        "hello@no-such-domain-4f7a2c.com",
        "not-an-email@",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("rod_analytics/bulk-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": [
    "info@apify.com",
    "example.user@gmial.com",
    "test@mailinator.com",
    "hello@no-such-domain-4f7a2c.com",
    "not-an-email@"
  ]
}' |
apify call rod_analytics/bulk-email-verifier --silent --output-dataset

```

## MCP server setup

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