# Email Validator: Bulk MX, Disposable and Role Check (`nightwave-owner/email-validator`) Actor

Returns a valid, risky or invalid verdict for each e-mail address, with syntax check, MX records, disposable domain and role account flags and typo suggestions (gmial.com to gmail.com). No SMTP probing, no mail sent.

- **URL**: https://apify.com/nightwave-owner/email-validator.md
- **Developed by:** [Viktor Wiberg](https://apify.com/nightwave-owner) (community)
- **Categories:** Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 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.

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 Validator: Bulk MX, Disposable and Role Check

Checks a list of e-mail addresses and gives each one a verdict: `valid`, `risky` or `invalid`, with the reason in plain words. The actor checks the syntax, looks up the domain's MX records in DNS, flags disposable (throwaway) domains and role accounts such as `info@` and `support@`, and suggests a fix for common domain typos such as `gmial.com` or `hotmail.con`.

No mail is sent and no connection is made to the recipient's mail server (no SMTP probing). That makes the actor fast, cheap and safe for your sending reputation, and it means it cannot tell whether a single mailbox exists. See "Good to know".

Use it to clean a list before a newsletter or campaign, to check sign-ups from a web form, or to validate the e-mail column from a lead scraper before it goes into your CRM.

### Example from a real run

This is the input and the full output of a run on the Apify platform on 3 October 2026 (run `dxBh4oyMgIenGY3bL`). The addresses are examples, not real people. Nothing in the output is edited.

Input:

```json
{
  "emails": [
    "kontakt@nightwave.se",
    "firstname.lastname@outlook.com",
    "Jane Doe <Jane.Doe@Gmial.com>",
    "someone@hotmail.con",
    "test@mailinator.com",
    "info@example.com",
    "name@nonexistent-domain-zzqx.se",
    "not-an-address"
  ]
}
```

Output (four of the eight rows):

```json
[
  {
    "email": "firstname.lastname@outlook.com",
    "normalized": "firstname.lastname@outlook.com",
    "isValidSyntax": true,
    "domain": "outlook.com",
    "hasMx": true,
    "mxHosts": ["outlook-com.olc.protection.outlook.com"],
    "isDisposable": false,
    "isRoleAccount": false,
    "suggestion": null,
    "status": "valid",
    "reason": "Valid syntax and the domain accepts e-mail (MX found)."
  },
  {
    "email": "Jane Doe <Jane.Doe@Gmial.com>",
    "normalized": "jane.doe@gmial.com",
    "isValidSyntax": true,
    "domain": "gmial.com",
    "hasMx": false,
    "mxHosts": [],
    "isDisposable": true,
    "isRoleAccount": false,
    "suggestion": "jane.doe@gmail.com",
    "status": "risky",
    "reason": "Disposable (throwaway) e-mail domain; possible typo, did you mean jane.doe@gmail.com; no MX record, mail would go to the domain's A/AAAA host."
  },
  {
    "email": "someone@hotmail.con",
    "normalized": "someone@hotmail.con",
    "isValidSyntax": true,
    "domain": "hotmail.con",
    "hasMx": false,
    "mxHosts": [],
    "isDisposable": false,
    "isRoleAccount": false,
    "suggestion": "someone@hotmail.com",
    "status": "invalid",
    "reason": "The domain does not exist. Did you mean someone@hotmail.com?"
  },
  {
    "email": "info@example.com",
    "normalized": "info@example.com",
    "isValidSyntax": true,
    "domain": "example.com",
    "hasMx": false,
    "mxHosts": [],
    "isDisposable": false,
    "isRoleAccount": true,
    "suggestion": null,
    "status": "invalid",
    "reason": "The domain publishes a null MX record: it accepts no e-mail."
  }
]
```

The other rows: `kontakt@nightwave.se` was `risky` (role account), `test@mailinator.com` was `risky` (disposable domain and role account), `name@nonexistent-domain-zzqx.se` was `invalid` (the domain does not exist) and `not-an-address` was `invalid` (missing @).

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `emails` | array | four examples | Addresses to check. `Name <address>` and `mailto:` links are accepted, and a pasted string with one address per line or separated by commas also works. Duplicates are checked and charged once. |
| `datasetId` | string | none | Also check the addresses in another Apify dataset, for example the output of a lead or contact scraper. |
| `emailField` | string | `email` | Field in that dataset that holds the address. A field with a list of addresses also works. |
| `onlyNew` | boolean | `false` | Check and charge only addresses that earlier runs have not checked. See "Monitoring and scheduling". |

Up to 100 000 unique addresses per run. A run with empty input checks four example addresses, which is handy for trying the actor from an AI agent or the Apify MCP server.

### Output

One row per unique address, in the order given.

| Field | Description |
|---|---|
| `email` | The address exactly as you sent it |
| `normalized` | Cleaned address: trimmed, without display name or `mailto:`, in lower case, with an international domain in its ASCII (punycode) form. `null` when the syntax is invalid |
| `isValidSyntax` | Whether the address follows the syntax of RFC 5321 and 5322 (dot-atom or quoted local part, UTF-8 local parts as in RFC 6531) |
| `domain` | The domain part, in ASCII form |
| `hasMx` | `true` when the domain has MX records, `false` when it has none, `null` when the lookup failed or the syntax was invalid |
| `mxHosts` | The MX hosts, sorted by priority |
| `isDisposable` | The domain, or a parent domain, is on the open list of disposable e-mail domains |
| `isRoleAccount` | The local part is a role address such as `info`, `admin`, `support`, `sales`, `noreply`, `kontakt` or `kundtjanst` (English and Nordic names, `+tags` ignored) |
| `suggestion` | The address with the likely intended domain when the domain looks like a typo of a large mailbox provider, for example `gmial.com` to `gmail.com` |
| `status` | `valid`, `risky` or `invalid` |
| `reason` | Why, in one sentence |

How the status is set:

- **invalid:** the syntax is wrong, the domain does not exist, the domain publishes a null MX (RFC 7505), or it has neither MX nor A/AAAA records.
- **risky:** the address can receive mail, but something speaks against sending to it: a disposable domain, a likely typo, a role account, a domain without MX that would get mail on its A record, a quoted local part, an IP address as domain, or a DNS lookup that did not answer.
- **valid:** correct syntax, the domain has MX records and none of the risks above apply.

### Monitoring and scheduling

Set `onlyNew` to `true` when you run the actor again and again on a growing list, for example new sign-ups or the output of a daily scraper. The actor then remembers which addresses it has checked, in a named key-value store in your Apify account (`nightwave-state-email-validator`), and checks and charges only addresses it has not seen before. Only a one-way SHA-256 hash of each normalized address is stored, never the address itself. The first run checks everything.

The addresses and the dataset ID are not part of the remembered input, so the same state is used however the list grows. To start over, delete the record in the key-value store.

Example: check the e-mail column of a lead scraper's latest dataset every morning at 06:00. In Apify Console, open **Schedules**, create a schedule with the cron expression `0 6 * * *` and add this actor with the input below.

```json
{
  "datasetId": "<the dataset ID or name>",
  "emailField": "email",
  "onlyNew": true
}
```

The same schedule through the Apify API:

```sh
curl -X POST "https://api.apify.com/v2/schedules?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"name": "daily-email-validation", "cronExpression": "0 6 * * *", "timezone": "Europe/Stockholm", "isEnabled": true, "isExclusive": true,
       "actions": [{"type": "RUN_ACTOR", "actorId": "nightwave-owner~email-validator",
                    "runInput": {"contentType": "application/json; charset=utf-8", "body": "<the input above as a JSON string>"}}]}'
```

You can also start the actor from another actor's webhook, so new leads are validated as soon as the scraper finishes.

### Good to know

- **No SMTP check.** The actor never connects to the recipient's mail server, so it cannot tell whether `anna@company.com` is a real mailbox, only that `company.com` accepts mail. Services that probe mailboxes over SMTP can catch more dead addresses, but they are slower, are blocked or answered falsely by many providers (Gmail, Outlook and most catch-all servers), and risk getting the checking IP blacklisted. If you need mailbox-level certainty, combine this actor with a double opt-in.
- **Disposable list.** About 9 200 domains from an open community list (see "Data source and license"), plus any subdomain of them. New throwaway services appear every week, so a domain that is not flagged can still be disposable.
- **Typo suggestions** cover the large mailbox providers (Gmail, Outlook, Hotmail, Live, Yahoo, iCloud, AOL, Proton, GMX and the big Nordic ones). A suggestion is only made when the provider name is one or two letters off and the top-level domain is the same or an obvious typo of it, so a real domain such as `live.de` is never "corrected".
- **Lower case.** `normalized` is in lower case. Strictly, the part before @ can be case-sensitive, but no large provider treats it that way.
- **DNS.** Each domain is looked up once per run, with 20 lookups in parallel, a 4 second timeout and one retry. A lookup that fails gives `risky` with `hasMx: null`, never `invalid`.
- **Privacy.** The output contains only what you sent in. The addresses are not written to the log, not stored anywhere outside your own run's dataset and not shared. With `onlyNew` only one-way hashes are kept. You are responsible for having a legal basis for the addresses you process (for example under GDPR).

### Use cases

- Clean a mailing list before a campaign, to lower the bounce rate and protect your sender reputation.
- Validate sign-ups from a form and stop throwaway addresses from taking free trials.
- Check the e-mail field of a lead or contact scraper's dataset before it is imported into a CRM.
- Separate role addresses (`info@`, `sales@`) from personal ones in a B2B list.
- Catch typing errors such as `gmial.com` and `hotmail.con` and ask the user to correct them.

### FAQ

**What does 1 000 addresses cost?**
1 USD (0.001 USD per address, event `email`), plus Apify platform usage. Platform usage is very small: a test run that checked 2 000 addresses across 315 domains took 4 seconds and used 0.0001 USD.

**Is a `valid` address guaranteed to be delivered?**
No. It means the address is well formed and its domain accepts mail. The mailbox itself is not checked.

**Can it check addresses from another actor?**
Yes. Give `datasetId` and `emailField`, and the actor reads the addresses from that dataset.

### Data source and license

The checks are the actor's own code: the syntax rules of RFC 5321, 5322 and 6531, DNS lookups of MX, A and AAAA records (RFC 5321 section 5.1 and RFC 7505), and a built-in list of role names and mailbox providers.

The disposable domain list is `disposable_email_blocklist.conf` from [github.com/disposable-email-domains/disposable-email-domains](https://github.com/disposable-email-domains/disposable-email-domains), downloaded on 3 October 2026 (9 203 domains). Its license, read on 3 October 2026, is CC0 1.0 Universal: "You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission." The list is bundled in the actor and updated with `scripts/update-disposable-domains.sh`.

### Pricing

Pay per result: 0.001 USD per checked address (event `email`), which is 1 USD per 1 000. Duplicates in the input are checked and charged once, and with `onlyNew` addresses checked in earlier runs are not charged again. Apify bills platform usage on top as usual.

### Contact

Built and maintained by Nightwave AB. Questions, bugs and feature requests: kontakt@nightwave.se

### På svenska

Actorn kontrollerar en lista med e-postadresser och ger varje adress ett utlåtande: `valid` (giltig), `risky` (riskabel) eller `invalid` (ogiltig), med skälet i klartext.

- Kontrollerna: syntax enligt RFC 5321, 5322 och 6531, MX-poster i DNS (med A/AAAA som reserv och null MX enligt RFC 7505), engångsdomäner, rollkonton (till exempel `info@`, `kontakt@`, `kundtjanst@`) och förslag när domänen ser ut som ett stavfel för en stor e-posttjänst (`gmial.com` till `gmail.com`).
- Ingen e-post skickas och ingen anslutning görs till mottagarens e-postserver (ingen SMTP-sondering). Actorn kan därför inte avgöra om en enskild brevlåda finns, bara att domänen tar emot e-post.
- Indata: en lista med adresser (`emails`) och/eller ett annat Apify-dataset (`datasetId` och `emailField`). Upp till 100 000 unika adresser per körning.
- Med `onlyNew: true` kontrolleras och debiteras bara adresser som inte har kontrollerats förut. Bara en envägshash av varje adress sparas, aldrig adressen.
- Utdata innehåller bara det du själv skickat in. Adresserna skrivs inte till loggen och sparas inte utanför din egen körnings dataset. Du ansvarar för att ha rättslig grund för de adresser du behandlar (till exempel enligt GDPR).
- Listan över engångsdomäner kommer från disposable-email-domains på GitHub och är licensierad CC0 1.0 (fri att använda kommersiellt), hämtad 3 oktober 2026.
- Pris: 0,001 USD per adress (1 USD per 1 000) plus Apifys plattformsanvändning.
- Kontakt: kontakt@nightwave.se

# Actor input Schema

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

Addresses to check, one per line, for example \["anna@example.com", "info@company.se"]. "Name <address>" and mailto: links are accepted. Duplicates are checked once. Up to 100 000 per run. Empty input checks four example addresses.

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

Also check addresses from another Apify dataset, for example the output of a lead scraper. Give the dataset ID or name, for example "aBcD1234efGh5678".

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

Name of the field in that dataset that holds the address, for example "email". A field with a list of addresses also works.

## `onlyNew` (type: `boolean`):

Check and charge only addresses that earlier runs have not checked. Only one-way hashes of checked addresses are stored, never the addresses. See "Monitoring and scheduling" in the README.

## Actor input object example

```json
{
  "emails": [
    "anna@example.com",
    "info@company.se"
  ],
  "datasetId": "aBcD1234efGh5678",
  "emailField": "email",
  "onlyNew": false
}
```

# Actor output Schema

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

One row per checked address, as JSON. Open in Apify Console or download via the dataset API.

# 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": [
        "kontakt@nightwave.se",
        "firstname.lastname@gmial.com",
        "test@mailinator.com",
        "not-an-address"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nightwave-owner/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": [
        "kontakt@nightwave.se",
        "firstname.lastname@gmial.com",
        "test@mailinator.com",
        "not-an-address",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("nightwave-owner/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": [
    "kontakt@nightwave.se",
    "firstname.lastname@gmial.com",
    "test@mailinator.com",
    "not-an-address"
  ]
}' |
apify call nightwave-owner/email-validator --silent --output-dataset

```

## MCP server setup

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