# Email Domain Checker (MX, SPF, DMARC) (`myuon/email-domain-check`) Actor

Check email addresses or domains for syntax, MX, SPF, DMARC, disposable and role flags. Does not connect to mail servers or confirm mailboxes.

- **URL**: https://apify.com/myuon/email-domain-check.md
- **Developed by:** [myuon](https://apify.com/myuon) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $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 Domain Check

Check a list of email addresses or bare domains for syntax problems and DNS-level deliverability signals, before you send anything. One result row per entry.

**This Actor checks syntax and DNS records only. It never connects to any mail server (no SMTP, no RCPT probing, no catch-all testing). It cannot tell you whether a particular mailbox exists.** A `likely_ok` verdict means the address is well-formed and its domain is set up to receive mail, nothing more.

### Who it is for

- People who clean a lead list or a signup list before sending, to drop typos, dead domains and domains that explicitly accept no mail.
- People who care about deliverability hygiene: spotting disposable domains and role addresses, and seeing whether a domain publishes SPF and DMARC.

It does not enrich people, find or guess addresses, or return any information about the person behind an address.

### What each row contains

| Field | Meaning |
| --- | --- |
| `input` | The entry exactly as you gave it (trimmed of surrounding spaces) |
| `domain` | Lower-case ASCII domain; internationalised domains are shown in punycode (`xn--...`) |
| `syntaxValid`, `syntaxError` | Whether it parses as an address or domain, and why not |
| `mxStatus` | `present`, `implicit_a`, `null_mx`, `none`, `nxdomain`, `unknown`, or `not_checked` (invalid syntax) |
| `mxPresent`, `mxRecords`, `nullMx` | MX presence, the MX list sorted by priority, and whether the domain publishes a Null MX |
| `spfPresent`, `spfRecord` | Whether a `v=spf1` TXT record exists, and its text |
| `dmarcPresent`, `dmarcPolicy` | Whether `_dmarc.<domain>` has a `v=DMARC1` record, and its `p=` value (`none`, `quarantine`, `reject`) |
| `disposable` | Domain, or a parent domain, is on the bundled disposable-domain list |
| `roleAddress` | Local part is a role mailbox such as `info`, `sales`, `support`, `noreply` (null for bare domains) |
| `freeProvider` | Domain is a well-known consumer mailbox provider (a built-in list that is not exhaustive) |
| `verdict`, `reasons` | `likely_ok`, `risky`, `invalid` or `unknown`, with machine-readable reasons |

#### Verdicts

- `invalid`: bad syntax, the domain does not exist, the domain publishes a Null MX (RFC 7505, it states it accepts no mail), or it has neither MX nor address records.
- `risky`: usable on paper, but disposable domain, role address, non-ASCII local part (needs SMTPUTF8), or no MX record and mail would fall back to the domain's A/AAAA record.
- `likely_ok`: valid syntax, the domain has MX records, and none of the risk flags apply.
- `unknown`: DNS timed out or the resolver failed (SERVFAIL etc.) after retries. This is not a statement that the address is bad. Unknown rows are delivered but **not charged**; run them again later.

Missing SPF or DMARC does not change the verdict; the fields are informational. DMARC is looked up at `_dmarc.<exact domain>` only; policy inherited from a parent domain is not followed.

#### MX fallback (RFC 5321 section 5.1)

If a domain has no MX record but does have an A or AAAA record, mail servers deliver to that address as an implicit MX. Such a domain gets `mxStatus: "implicit_a"` and verdict `risky` (reason `no_mx_uses_address_record_fallback`), not `invalid`.

#### Syntax rules

Dot-atom local parts up to 64 bytes, plus signs allowed (`name+tag@example.com`), non-ASCII local parts allowed and flagged, internationalised domains converted to punycode. Quoted local parts (`"a b"@example.com`), display names (`Name <a@b.com>`) and IP-literal domains (`a@[1.2.3.4]`) are reported as invalid. A bare domain (no `@`) is accepted and checked as a domain.

### Input

| Field | Meaning | Default |
| --- | --- | --- |
| `emails` | Addresses or bare domains, one per entry. Up to 50,000 per run; a longer list is rejected before anything is processed | required |
| `dedupe` | Skip entries identical to an earlier one | on |

#### Duplicates

With `dedupe` on, an entry that matches an earlier one after trimming and lower-casing produces no row and is not charged; the number skipped is in the run summary. Different addresses on the same domain are never merged: each gets its own row, and they share a single DNS lookup (results are cached by domain within the run). Blank entries are always skipped. With `dedupe` off, every non-blank entry gets a row and is charged.

### Pricing

Pay per event: **`email-checked`, suggested $0.0008 per row**, charged once per row delivered to the dataset. See `.actor/pay_per_event.json`.

- Rows for syntactically invalid entries are delivered and **are charged**.
- Rows with verdict `unknown` (DNS timeout or failure) are delivered but **not charged**.
- Skipped duplicates, blank lines and entries not processed are not charged.
- The Actor respects your maximum charge per run. When the next row would exceed it, the run stops cleanly, and the status message and the `SUMMARY` record in the key-value store say how many entries were not processed. Rows already delivered stay in the dataset.

The price is set in Apify Console, so check the Actor's pricing tab for the current value.

### Limits

- 50,000 entries per run.
- Only DNS is queried, with a per-query timeout and up to two retries. A resolver that is slow or rate-limited for a particular domain gives `unknown` for that domain only.
- The disposable list is a snapshot (see below) and the free-provider and role lists are short built-in lists. None of them is complete.
- The default test input uses `iana.org` and `icann.org` (which publish MX records) and two `example.com`/`example.org` addresses (reserved domains that publish a Null MX, so they come back `invalid`). Every entry yields a row, so the dataset is never empty.

### Privacy

Your list stays in your own run storage (dataset and key-value store). Local parts are used in memory for the syntax and role checks and appear only in your own dataset rows. Logs contain counts only, never addresses or local parts. The only network traffic is DNS queries for the domains in your list; the domain names are visible to the DNS resolver, as with any DNS lookup. No mail server is contacted and no email is sent.

### Your responsibility

You are responsible for complying with the email and privacy laws that apply to you, for example Japan's Act on Regulation of Transmission of Specified Electronic Mail (特定電子メール法), the US CAN-SPAM Act, and the EU GDPR. This tool is not for sending, or enabling the sending of, unsolicited mail. A result from this Actor is not consent, and does not make a list lawful to use.

### Data sources

- Disposable domains: the community list at https://github.com/disposable-email-domains/disposable-email-domains (CC0 1.0), obtained through the npm package `disposable-email-domains-js` 1.26.0 (published 2026-09-21), vendored in `data/disposable-domains.json`. Licence and provenance are in `data/`.

# Actor input Schema

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

Email addresses (name@example.com) or bare domains (example.com), one per line. Up to 50,000 per run; larger lists are rejected, so split them. Each entry is checked for syntax and its domain is looked up in DNS. Nothing is sent to any mailbox and no mail server is contacted.

## `dedupe` (type: `boolean`):

When on, an entry that is identical to an earlier one (ignoring case and surrounding spaces) produces no row and is not charged. Blank lines are always skipped. Different addresses on the same domain are never merged; they share one DNS lookup.

## Actor input object example

```json
{
  "emails": [
    "iana.org",
    "icann.org",
    "info@example.com",
    "user@example.org"
  ],
  "dedupe": true
}
```

# Actor output Schema

## `frames` (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": [
        "iana.org",
        "icann.org",
        "info@example.com",
        "user@example.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("myuon/email-domain-check").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": [
        "iana.org",
        "icann.org",
        "info@example.com",
        "user@example.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("myuon/email-domain-check").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": [
    "iana.org",
    "icann.org",
    "info@example.com",
    "user@example.org"
  ]
}' |
apify call myuon/email-domain-check --silent --output-dataset

```

## MCP server setup

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

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/LkJJQaSQEIWvbbqbk/builds/xps6E8ygSdGGDbXmy/openapi.json
