# Bulk Email Validator: MX, Disposable, Role & Typo Check (`yourname_mahi/bulk-email-validator`) Actor

Clean email lists with syntax + IDN, real DNS mail-routing (MX / Null MX), disposable, role and free-provider flags, typo hints and reason codes. No proxy, no SMTP probing, no false 'mailbox exists' claims. Pay per email checked; unknown results are free.

- **URL**: https://apify.com/yourname\_mahi/bulk-email-validator.md
- **Developed by:** [MST MORIUM AKTHER MAYA](https://apify.com/yourname_mahi) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 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

## Bulk Email Validator: MX, Disposable, Role & Typo Check

Clean an email list **before** you send to it. This Actor checks every address for
bad syntax, dead domains, disposable domains, role mailboxes (`info@`, `sales@`),
and likely typos (`gmail.con`), and tells you **why** for each row.

It uses **only public DNS**. No proxy, no API keys, no third-party service, no
email is ever sent.

> **Important:** this Actor checks the *address format and the domain*, not the mailbox.
> `valid` means "well-formed, and the domain can receive email". It does **not** mean
> "this person's inbox exists". See "What valid means here" below.

**Quick start:** open the **Input** tab, paste your addresses into *Email addresses*
(or *Paste emails*), press **Start**, then open the **Output** tab. Each row has a
`status` and a `reason` that tell you what to do with that address.

### What "valid" means here (please read)

> **`valid` = the address is well-formed AND its domain publishes a working way to receive mail.
> The mailbox itself is NOT checked.** Every row carries `"mailboxChecked": false`.

Nobody can tell from DNS alone whether `john@company.com` exists. Tools that claim
to do it use SMTP probing, which is unreliable (cloud hosts block port 25, big
providers accept everything, many servers greylist or lie) and is not part of
this Actor. What you get instead is a set of **honest, deterministic signals**
that remove the addresses that are *certainly* bad and flag the ones that are *probably* risky.

This Actor does **not**: test mailboxes via SMTP, detect catch-all servers,
detect spam traps, or guarantee inbox delivery.

### What it checks

| Check | Result |
|---|---|
| Syntax (RFC 5321/5322/6531) | `invalid` with a precise reason (`missing_at_sign`, `consecutive_dots_in_local_part`, ...) |
| Internationalised domains (IDN) | `bücher.de` is converted to punycode and checked |
| DNS mail routing | `MX` lookup, RFC 7505 **Null MX**, A/AAAA fallback, dangling MX hosts |
| Disposable domains | Bundled community list (CC0), including subdomains |
| Role accounts | `info@`, `support@`, `noreply@` ... -> `risky` |
| Free providers | `gmail.com`, `yahoo.com` ... flagged in `isFreeProvider` (informational, never lowers the status) |
| Typos | Conservative "did you mean" for near-misses of major providers only |
| Duplicates | Detected after normalisation (case, spaces, `Name <a@b.com>` wrappers). Skipped by default, or kept and marked with `duplicateOf`. Never charged |

### Statuses

| status | meaning | typical action |
|---|---|---|
| `valid` | Syntax OK and the domain can receive mail. Mailbox not checked. | keep |
| `risky` | Usable but has a risk signal (see `reasons`): `role_account`, `possible_typo`, `no_mx_record_uses_a_record`, `internationalized_local_part` | your call |
| `invalid` | Syntax error | remove |
| `undeliverable_domain` | Domain does not exist, has no mail records, or publishes Null MX (`domain_not_found`, `no_mail_records`, `null_mx`, `mx_hosts_unresolvable`) | remove |
| `disposable` | Throw-away mail domain | remove |
| `unknown` | We could not decide (DNS timeout / server failure). **Not charged.** | re-run later |

`confidence` (`high` / `medium` / `low`) says how sure we are about the **status**,
not about the mailbox. `valid` is at most `medium` because the mailbox is unverified.

### Input

Use any combination of:

- **`emails`**: list of addresses (best for API / MCP / AI-agent use)
- **`emailsText`**: paste a column (new lines, commas or semicolons)
- **`datasetId`** + **`emailField`**: read emails from another Actor's dataset
  (e.g. a lead scraper). The field may hold one email or a list, and can be nested (`contact.email`).

For big lists (thousands of rows) prefer `emailsText` or `datasetId`; the list editor is meant for short lists.

Other options: `includeDuplicates`, `maxEmails` (default 100,000, max 200,000; if your list is longer, the run summary and status message tell you how many rows were not processed), `dnsConcurrency`.

```json
{
  "emails": ["support@apify.com", "someone@mailinator.com", "john.doe@gmail.con"]
}
```

### Output

One dataset row per unique address, **in input order**, with a stable `index`
(the position among your non-blank input rows, starting at 0; blank rows are dropped before
numbering, so if your list contains blanks, join back on the `input` field rather than on `index`):

```json
{
  "index": 2,
  "input": "john.doe@gmail.con",
  "email": "john.doe@gmail.con",
  "status": "undeliverable_domain",
  "reason": "domain_not_found",
  "reasons": ["domain_not_found"],
  "confidence": "high",
  "mailboxChecked": false,
  "isValidSyntax": true,
  "domain": "gmail.con",
  "hasMx": false,
  "mxRecords": [],
  "mxProvider": null,
  "isDisposable": false,
  "isRoleAccount": false,
  "isFreeProvider": false,
  "suggestion": "john.doe@gmail.com",
  "normalizations": [],
  "duplicateOf": null
}
```

A run **summary** (counts per status, duplicates, rows charged, whether the run
stopped at your max charge) is saved in the key-value store under `SUMMARY`.

### Pricing (pay per event)

You pay per **unique address that received a conclusive result**. That includes `invalid`,
`undeliverable_domain` and `disposable` rows, because telling you an address is bad *is* the result.
**Free:** duplicates, blank rows, and `unknown` rows (DNS problems on our side of the check).
If you set a *maximum cost per run*, the Actor stops cleanly when it is reached and
never charges beyond it. Rows already checked stay in the dataset.

See the **Pricing** tab for the current per-email price. At the time of writing it is
$0.70 per 1,000 checked addresses, plus a tiny fixed start fee of $0.00005 per run.
For example, 10,000 unique addresses cost about $7.00. A list of 1,000 addresses where 200 are
duplicates and 30 could not be decided (`unknown`) is charged for 770 addresses only.

### Use it with other Actors

**Lead scraper -> this Actor -> clean list.** Pass the scraper's dataset ID in
`datasetId` and the field name in `emailField`. The output keeps your rows in order
and repeats the original value in `input`, so you can join it back to your source data.
The dataset must be one your own Apify account can read.

### Use it from code, automation tools and AI agents

The input is one plain JSON object and every output row is flat JSON with fixed status
values and reason codes, so scripts and AI agents can act on it without parsing prose.
The Actor needs no secrets and runs with limited permissions.

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("yourname_mahi/bulk-email-validator").call(
    run_input={"emails": ["support@apify.com", "john@gmail.con"]}
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["status"], row["reason"], row["suggestion"])
```

**JavaScript**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<APIFY_TOKEN>' });
const run = await client.actor('yourname_mahi/bulk-email-validator').call({
    emails: ['support@apify.com', 'john@gmail.con'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((r) => [r.status, r.reason]));
```

**cURL** (small lists; this endpoint waits for the result, so keep it to a few thousand addresses)

```bash
curl -X POST "https://api.apify.com/v2/acts/yourname_mahi~bulk-email-validator/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["support@apify.com", "john@gmail.con"]}'
```

**n8n / Make / Zapier:** use the Apify integration's *Run Actor* step with the JSON input above,
then *Get dataset items*, and branch on the `status` field (for example keep only `valid` and `risky`).

**AI agents:** Actors can be called through the Apify MCP server. Pass `emails` as a list and read
`status`, `reason` and `confidence` from each row; `mailboxChecked` is always `false`.

### Large lists

- Use *Paste emails* or a dataset (`datasetId`) instead of the list editor for thousands of rows.
- Domains are looked up once and cached, so lists with many repeated domains finish very fast.
- Set a *Maximum cost per run* in the run options to cap spending. The Actor stops cleanly at the
  limit and tells you how many rows were checked. Rows already checked stay in the dataset.
- If many rows come back `unknown`, re-run only those rows later, or lower `dnsConcurrency`. `unknown` rows are free.

### FAQ

#### Can I use `valid` to guarantee delivery?

No. It means "worth sending to", not "the mailbox exists".

#### Why not SMTP mailbox checks?

They are unreliable from cloud infrastructure and cannot be presented honestly as proof. We prefer fewer, correct claims.

#### Is my list stored or shared?

Addresses are processed in memory and written only to your run's dataset. They are never written to logs
and never sent to any third-party service. Lookups go to public DNS: the *domain* part of your addresses is
visible to the DNS resolver, as with any DNS query.

#### How fast is it?

Domains are checked once and cached, so lists with repeated domains are very fast (thousands per second);
lists where every address has a different domain are limited by DNS speed.

#### How complete are the lists?

The disposable, free-provider and role lists are bundled and not exhaustive; new disposable domains appear constantly.

#### Why is a real-looking address marked `risky`?

Look at `reasons`. Typical causes: a role address (`info@`), a possible typo of a big provider (`gmial.com`),
or a domain with no MX record where mail would fall back to the domain's web server address.

# Actor input Schema

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

List of email addresses to check. One address per item. Duplicates are detected automatically.

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

Paste addresses separated by new lines, commas or semicolons (e.g. a column copied from a spreadsheet).

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

ID of an Apify dataset your account can read, for example the dataset produced by a lead-scraper run. Emails are read from the field named below. Values that are lists of emails are supported.

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

Field that holds the email in each dataset item. Use dots for nested fields, e.g. 'contact.email'. The field may be a single email or a list of emails.

## `includeDuplicates` (type: `boolean`):

Off (default): each unique address appears once and duplicates are skipped. On: duplicates are kept in input order, marked with 'duplicateOf' (the index of the first occurrence). Duplicates are never charged.

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

Safety cap. Only the first N input rows are processed. For very large lists split them across several runs.

## `dnsConcurrency` (type: `integer`):

How many domains are looked up in parallel. The default is fine for almost everyone; lower it if you see many 'unknown' results caused by DNS timeouts.

## Actor input object example

```json
{
  "emails": [
    "support@apify.com",
    "info@apify.com",
    "someone@mailinator.com",
    "john.doe@gmail.con",
    "not-an-email"
  ],
  "emailField": "email",
  "includeDuplicates": false,
  "maxEmails": 100000,
  "dnsConcurrency": 50
}
```

# Actor output Schema

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

One row per unique address with status, reason codes, confidence and risk flags.

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

Counts per status, duplicates removed, rows charged and whether the run stopped early at your max charge.

# 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": [
        "support@apify.com",
        "info@apify.com",
        "someone@mailinator.com",
        "john.doe@gmail.con",
        "not-an-email"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("yourname_mahi/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": [
        "support@apify.com",
        "info@apify.com",
        "someone@mailinator.com",
        "john.doe@gmail.con",
        "not-an-email",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("yourname_mahi/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": [
    "support@apify.com",
    "info@apify.com",
    "someone@mailinator.com",
    "john.doe@gmail.con",
    "not-an-email"
  ]
}' |
apify call yourname_mahi/bulk-email-validator --silent --output-dataset

```

## MCP server setup

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