# Email Validator API: Syntax, MX and Disposable Check (`thesjostrandproject/email-validator`) Actor

Validate email addresses by syntax, MX record and disposable-domain list and return one row per address with a valid, risky, invalid or unknown verdict. Input: up to 5,000 addresses. Row: verdict, reason codes, MX hosts, disposable, role and typo hint. No SMTP check.

- **URL**: https://apify.com/thesjostrandproject/email-validator.md
- **Developed by:** [The Sjöstrand Project](https://apify.com/thesjostrandproject) (community)
- **Categories:** Agents, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.80 / 1,000 email checkeds

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 API: Syntax, MX and Disposable Check

### What this Actor returns

Validate email addresses by syntax, MX record and disposable-domain list and return one row per address with a valid, risky, invalid or unknown verdict. You give it a list of up to 5,000 addresses; each row holds the verdict, the reason codes behind it, whether the domain exists and accepts mail, its mail servers, and whether the address is disposable, a role address or one typo away from a large mail provider.

- `email`, `verdict`, `reasons`: the address as you gave it, the verdict and its stable reason codes
- `domain_found`, `accepts_mail`, `mx_hosts`: what DNS says about the domain's mail
- `is_disposable`, `is_role`, `suggested_domain`: the disposable flag, the role flag and a typo hint ("possible typo; this domain does not receive mail")
- `list_version`, `checked_at`: the disposable list's date and commit, and when the row was made

No SMTP check: this Actor never connects to a mail server, so "valid" means the domain accepts mail, not that the mailbox exists.

### Who it helps

- **Anyone cleaning a sign-up or contact list** who wants to drop addresses whose domain cannot take mail, flag disposable ones and catch typos such as a misspelt provider, before sending anything.
- **Developers and AI agents** who need a quick, predictable check inside a form or a pipeline, through the Apify API, with the same fields and codes on every row.

### Quick start

1. Open the Input tab and leave the default list as it is. It holds four example lines on reserved names, so a first run sends no DNS query at all and returns four rows.
2. Press Start. The default run costs about $0.0025: three checked addresses at $0.0008 (the line that is not an address is free), plus Apify's tiny start event.
3. Open the Output tab, look at the verdicts and reasons, then paste in your own addresses, one per line.

### Input

| Field | What it does | Default |
|---|---|---|
| Email addresses | The addresses to check, one per line, up to 5,000 per run. A line that is not one address gets an invalid row and is not charged; neither is a row whose DNS lookup failed (`unknown`). Repeated addresses are checked once. | four example lines on reserved names |

### Output

One row per unique address, in the order you gave them. The rows below are what the default input returned on 13 October 2026, unchanged.

```json
[
  {
    "email": "jane.doe@example.com",
    "verdict": "invalid",
    "reasons": ["null_mx", "reserved_domain"],
    "domain_found": true,
    "accepts_mail": false,
    "mx_hosts": [],
    "is_disposable": false,
    "is_role": false,
    "suggested_domain": null,
    "list_version": "2026-10-13 d1a9c51",
    "checked_at": "2026-10-13T10:13:35Z"
  },
  {
    "email": "info@example.org",
    "verdict": "invalid",
    "reasons": ["null_mx", "role_address", "reserved_domain"],
    "domain_found": true,
    "accepts_mail": false,
    "mx_hosts": [],
    "is_disposable": false,
    "is_role": true,
    "suggested_domain": null,
    "list_version": "2026-10-13 d1a9c51",
    "checked_at": "2026-10-13T10:13:35Z"
  },
  {
    "email": "not-an-address",
    "verdict": "invalid",
    "reasons": ["syntax_invalid"],
    "domain_found": null,
    "accepts_mail": null,
    "mx_hosts": [],
    "is_disposable": false,
    "is_role": false,
    "suggested_domain": null,
    "list_version": "2026-10-13 d1a9c51",
    "checked_at": "2026-10-13T10:13:35Z"
  },
  {
    "email": "someone@nothing.invalid",
    "verdict": "invalid",
    "reasons": ["domain_not_found", "reserved_domain"],
    "domain_found": false,
    "accepts_mail": false,
    "mx_hosts": [],
    "is_disposable": false,
    "is_role": false,
    "suggested_domain": null,
    "list_version": "2026-10-13 d1a9c51",
    "checked_at": "2026-10-13T10:13:35Z"
  }
]
```

#### Verdicts and reason codes

The verdict is the most serious reason on the row: `invalid`, then `unknown`, then `risky`; a row with none of those is `valid`.

| Code | Meaning | Verdict |
|---|---|---|
| `syntax_invalid` | The line is not one address. | invalid |
| `domain_not_found` | The domain does not exist in DNS. | invalid |
| `null_mx` | The domain says it takes no mail (a null MX record). | invalid |
| `no_mail_host` | The domain exists but has no MX, A or AAAA record. | invalid |
| `dns_error` | The DNS lookup failed, so nothing can be said. The row is not charged. | unknown |
| `implicit_mx` | No MX record, but an A or AAAA record that mail may fall back to. | risky |
| `disposable_domain` | The domain is on the disposable-address list. | risky |
| `role_address` | The part before the @ is a role name such as info or support. | no change |
| `reserved_domain` | A reserved example name, answered without any DNS query. | no change |

### What it costs

You pay for one event, **Email checked**, at $0.0008 per unique address checked. A line that is not an address, or whose DNS lookup failed (an `unknown` row), still gets its row and is never charged. Repeated addresses are checked and charged once. Apify also adds its standard run-start event; it is the platform's own event, not ours.

For example, a list of 1,000 unique addresses costs $0.80. If your Apify plan includes monthly credit, a small run draws on that credit.

If the run stops early (see below), the rows already returned stand and nothing more is charged.

### Limits and honest notes

- **No mailbox check.** Without SMTP the Actor cannot tell whether a mailbox exists or is full; it tells you whether the domain can take mail at all. Tools that probe mail servers give finer answers and touch the recipient's server; this one never does.
- **Up to 5,000 addresses per run**, each at most 254 characters. A longer list, or a line that is not text, stops the run before anything is checked and nothing is charged. For more, split the list.
- **One lookup per domain.** Addresses are grouped by domain, so a list from one company asks DNS once. Lookups go out at most 20 a second, four at a time.
- **The resolver's limits are not published.** If it keeps answering that it is busy, the run stops with a clear message; the rows already returned stand and the rest are not charged.
- **Strict syntax.** Quoted local parts and non-ASCII characters before the @ are counted as not an address. An address whose domain ends in a dot (a dot after the last label) is counted as not an address (it is not normalised). International domain names after the @ are fine.
- **Typo hints** cover about twenty of the largest mail providers, one typing slip away, and appear only when the typed domain does not receive mail (no MX and no A or AAAA record): possible typo; this domain does not receive mail. A domain that resolves is never hinted, so a short real domain is not taken for a provider's.
- **Disposable domains come from a bundled copy of the list,** refreshed in a new build; `list_version` on each row shows which copy was used.

### Privacy

Privacy: we don't keep your inputs. Addresses are used only to produce this run's results, which stay in your Apify storage, and are never written to the run log or status messages.

Only the domain part of each address is sent to Cloudflare's public DNS resolver; the part before the @ never leaves the run.

We open a run's input, log or results only if you share the run with us, for example by attaching it to an issue. Apify's privacy policy applies to the platform itself.

Validate only addresses you have a lawful basis to process. This Actor does not find, extract or guess addresses.

### Questions

**Why is an address "valid" when mail to it bounces?**
"Valid" means the domain publishes a mail server. The mailbox itself may not exist; only a mail server could say, and this Actor never asks one.

**What does each status message mean?**
"Invalid input: the list of addresses is missing or is not a list" means the `emails` field must be a list of text lines. "every entry in the list must be text" means a line is something else, such as a number; "an entry is longer than one address can be" means a line is over 254 characters; "the list holds more entries than one run accepts" means more than 5,000 lines. Nothing was charged in any of these. "the DNS resolver's request limit was reached" means the resolver kept saying it was busy; wait a few minutes and run the remaining addresses again.

**Why is a row "unknown"?**
The DNS lookup for its domain failed, for example because the domain's own name servers did not answer. Try that address again later: an `unknown` row is not charged, so checking it again costs nothing extra.

**Why is a role address not marked risky?**
Role addresses such as info or support are often real, shared inboxes. The row flags them with `is_role` and the `role_address` code so you can decide.

**Can I run it on a schedule or from an AI agent?**
Yes, with the Apify scheduler, the API or an agent integration. Each run is independent and charged per unique address checked.

### Data source and AI note

Disposable domains come from the disposable-email-domains list (CC0 1.0).

DNS answers come from Cloudflare's public DNS resolver, asked only about the domain part of each address.

This tool was built with the help of AI coding assistants.

### Help

Questions, a verdict that looks wrong, or an idea: please open an issue on the Issues tab. Thank you for the report; every one is read.

# Actor input Schema

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

The addresses to check, one per line, up to 5,000 per run. Each line is one address; a line that is not one address gets an invalid row and is not charged. Repeated addresses are checked once. The default lines are reserved example names, so a first run sends no DNS query.

## Actor input object example

```json
{
  "emails": [
    "jane.doe@example.com",
    "info@example.org",
    "not-an-address",
    "someone@nothing.invalid"
  ]
}
```

# Actor output Schema

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

One row per unique address with its verdict, reason codes and DNS findings.

# 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": [
        "jane.doe@example.com",
        "info@example.org",
        "not-an-address",
        "someone@nothing.invalid"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("thesjostrandproject/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": [
        "jane.doe@example.com",
        "info@example.org",
        "not-an-address",
        "someone@nothing.invalid",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("thesjostrandproject/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": [
    "jane.doe@example.com",
    "info@example.org",
    "not-an-address",
    "someone@nothing.invalid"
  ]
}' |
apify call thesjostrandproject/email-validator --silent --output-dataset

```

## MCP server setup

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