# Bulk Email Verifier - MX, SPF, DMARC & Disposable (`thedeadpoet/bulk-email-verifier-mx-spf-dmarc-disposable-check`) Actor

Clean a bulk email list without sending anything. One row per address: MX records and mailbox provider, SPF and DMARC policy, disposable-domain and role-address flags, typo correction, and a 0-100 deliverability score. No SMTP probe, no sender reputation risk.

- **URL**: https://apify.com/thedeadpoet/bulk-email-verifier-mx-spf-dmarc-disposable-check.md
- **Developed by:** [Pablo D](https://apify.com/thedeadpoet) (community)
- **Categories:** Lead generation, Automation
- **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 address 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bulk Email Verifier — MX, SPF, DMARC and disposable-domain checks, without sending a thing

**You have a list of email addresses and no idea how many of them will bounce.** Some are typos. Some are `mailinator.com` throwaways someone used to get past a signup form. Some point at domains that were sold, parked or shut down two years ago. Send to that list and you burn your sender reputation on the first campaign.

This Actor takes the list and returns **one clean row per address** — MX records, mailbox provider, SPF and DMARC policy, disposable/role/free-provider flags, a typo correction, and a 0–100 score — using **only DNS lookups and offline rules**.

```
sales@stripe.com        ─┐                    ┌─ status: risky   (role address)
paul@personio.de        ─┼──►  one row each ──┼─ status: accepts_mail, score 100, Google Workspace
someone@gmial.com       ─┼──►                 ├─ status: risky   → did you mean someone@gmail.com
test@mailinator.com     ─┘                    └─ status: invalid (disposable)
```

### Bulk email verifier that never opens an SMTP connection

Most email verifiers work by opening an SMTP session to the recipient's mail server and issuing `RCPT TO` without ever sending the message. It is the only way to test whether a specific *mailbox* exists — and it comes with real costs that are rarely mentioned on the tin:

- Large providers (Google, Microsoft) answer `250 OK` for **every** address, so the result is meaningless on the domains most of your list uses.
- Greylisting and rate limiting produce false "invalid" verdicts on perfectly good addresses.
- The probing IP accumulates a reputation. On a shared cloud platform, that reputation is not yours alone.

**This Actor deliberately does none of that.** Every check here is a DNS query or a rule applied offline. Nothing connects to a mail server, so running it cannot get an IP blocklisted and cannot damage your sending reputation. What it gives you in exchange is the part of list hygiene that is both reliable and where most of the bounces actually come from: **domains that cannot receive mail at all, throwaway providers, role inboxes, and typos**.

`isCatchAllUnknown` is `true` on every row, permanently, as a standing reminder of the boundary: **this Actor never claims a specific mailbox exists.**

### What it checks

| Check | Field | How |
|---|---|---|
| Syntax | `isValidSyntax`, `syntaxError` | Practical RFC 5322 subset — deliberately stricter than the RFC, because quoted local parts and IP-literal domains never appear in real CRM data |
| Domain exists | `domainExists` | NXDOMAIN detection |
| Can receive mail | `hasMxRecord`, `mxHosts`, `mxCount`, `nullMx` | MX lookup, including RFC 7505 null-MX ("this domain accepts no mail") |
| Mailbox provider | `mailProvider` | 45+ MX fingerprints: Google Workspace, Microsoft 365, Zoho, Proton, Fastmail, Mimecast, Proofpoint, Barracuda, IONOS, OVH, Rackspace, Amazon SES, Migadu, Cloudflare Email Routing and more |
| Disposable | `isDisposable` | 180+ throwaway providers — Mailinator, Guerrilla Mail, 10minutemail, YOPmail, temp-mail, Trashmail … |
| Role address | `isRoleAddress` | 90+ shared-inbox local parts — `info@`, `sales@`, `support@`, `noreply@`, `hr@` … |
| Free consumer mailbox | `isFreeProvider` | 130+ consumer domains, so you can tell a company contact from a personal Gmail |
| Typo | `suggestedCorrection`, `suggestedEmail` | Damerau-Levenshtein against the domains typos actually land next to (`gmial.com` → `gmail.com`) |
| SPF | `hasSpfRecord`, `spfRecord`, `spfPolicy` | TXT lookup, plus the qualifier on the `all` mechanism |
| DMARC | `hasDmarcRecord`, `dmarcRecord`, `dmarcPolicy` | `_dmarc` TXT lookup, plus the `p=` value |
| MTA-STS | `hasMtaSts` | `_mta-sts` TXT lookup |

DNS results are cached **per domain**, so a 10,000-row list of 400 companies costs 400 domains' worth of lookups, not 10,000.

### Input

```json
{
  "emails": [
    "sales@stripe.com",
    "info@apify.com",
    "someone@gmial.com",
    "test@mailinator.com",
    "hello@this-domain-does-not-exist-9x8y7z.com"
  ],
  "domains": ["nvidia.com"],
  "checkSpfDmarc": true,
  "treatDisposableAsInvalid": true,
  "treatRoleAddressAsRisky": true,
  "deduplicate": true,
  "onlyStatuses": [],
  "concurrency": 10,
  "dnsTimeoutSeconds": 5,
  "dnsResolvers": ["1.1.1.1", "8.8.8.8", "9.9.9.9"]
}
```

`domains` is for when you want a domain's mail setup without a specific mailbox — the row comes back with `email: null` and every DNS field filled in.

### Output

```json
{
  "input": "someone@gmial.com",
  "email": "someone@gmial.com",
  "localPart": "someone",
  "domain": "gmial.com",
  "status": "risky",
  "score": 30,
  "reasons": [
    "no MX record; mail would fall back to the A record, which often fails",
    "looks like a typo for gmail.com",
    "domain publishes no DMARC record"
  ],
  "isValidSyntax": true,
  "syntaxError": null,
  "isDisposable": false,
  "isRoleAddress": false,
  "isFreeProvider": false,
  "isCatchAllUnknown": true,
  "suggestedCorrection": "gmail.com",
  "suggestedEmail": "someone@gmail.com",
  "domainExists": true,
  "domainResolves": true,
  "hasMxRecord": false,
  "mxHosts": [],
  "mxCount": 0,
  "mailProvider": null,
  "nullMx": false,
  "aRecords": ["51.79.68.169"],
  "hasSpfRecord": false,
  "spfRecord": null,
  "spfPolicy": null,
  "hasDmarcRecord": false,
  "dmarcRecord": null,
  "dmarcPolicy": null,
  "hasMtaSts": false,
  "dnsError": null,
  "checkedAt": "2026-09-10T17:12:05.740860+00:00"
}
```

### What the four statuses mean

| `status` | Meaning | What to do |
|---|---|---|
| `accepts_mail` | The domain has valid MX and no risk flags. Mail will reach the server. | Send. The mailbox itself is unverified — see the limitations below. |
| `risky` | Deliverable in principle, but flagged: a role inbox, a free consumer mailbox, a probable typo, or a domain with no MX at all. | Review. `reasons` says exactly which. |
| `invalid` | Syntax failure, NXDOMAIN, a null MX, or a disposable provider. | Remove from the list. |
| `unknown` | DNS timed out or returned SERVFAIL. | Re-run. A DNS failure never produces a false `invalid`. |

### Honest limitations

- **Mailbox existence is not checked.** `jane.doe@stripe.com` and `zzzz@stripe.com` both come back `accepts_mail`, because both would be accepted by the same MX hosts. If you need per-mailbox verification, this Actor is the wrong tool and says so rather than guessing.
- **Catch-all detection is impossible here** for the same reason. `isCatchAllUnknown` is always `true`.
- **The disposable list is a snapshot**, not a live feed. New throwaway domains appear constantly; 180+ of the most common ones are covered.
- **`isFreeProvider` is not a quality judgement.** Plenty of real sole traders run their business on Gmail. It is there so you can segment, not so you can delete.
- **Typo correction is conservative.** It only fires within two edits of a domain that typos genuinely land near, and never on domains shorter than six characters, where a one-character difference is usually a different company rather than a mistake.

### Works with

- [Website Tech Stack & Email Finder](https://apify.com/thedeadpoet/website-tech-stack-email-finder-wappalyzer-builtwith) — finds the addresses on a company's own site. Feed its `emails` array straight into this Actor's `emails` input and you have a found-and-verified list in two runs.
- [SEC EDGAR Form D — Startup Funding Rounds API](https://apify.com/thedeadpoet/sec-edgar-form-d-startup-funding-rounds-api) — companies that just raised money, with the officers named on the filing.

### FAQ

**Does this send test emails?**
No. Nothing is sent and no SMTP connection is opened. Only DNS is queried.

**Why is `sales@stripe.com` "risky" when Stripe obviously exists?**
Because `sales@` is a role address that reaches a shared inbox rather than a person. Set `treatRoleAddressAsRisky: false` if that is what you want in your list.

**How many addresses can it handle?**
Up to 10,000 per run. Because results are cached per domain, throughput is governed by how many *unique domains* the list contains, not how many addresses.

**Can I use my own DNS resolver?**
Yes — put its IP in `dnsResolvers`, or use the single entry `"system"` to use the container's own resolver.

**Will an address that comes back `accepts_mail` definitely not bounce?**
No, and nothing that does not open an SMTP session can promise that. It means the domain will accept the connection. Whether that specific mailbox exists is not knowable from DNS.

**What does it cost?**
Pay per event: a fixed fee to start the run, plus a small fee per address checked. No proxy costs — DNS is free.

# Actor input Schema

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

One address per line. Surrounding angle brackets and a leading "mailto:" are stripped automatically.

## `domains` (type: `array`):

Optional. Check a bare domain's mail setup without a specific mailbox - MX, SPF, DMARC and provider. The output row has email = null and domain set.

## `checkSpfDmarc` (type: `boolean`):

Adds three more DNS lookups per unique domain and fills spfRecord, spfPolicy, dmarcRecord, dmarcPolicy and hasMtaSts. Turn off for the fastest possible MX-only pass.

## `treatDisposableAsInvalid` (type: `boolean`):

On: a mailinator.com or temp-mail.org address gets status "invalid". Off: it gets "risky" and you decide.

## `treatRoleAddressAsRisky` (type: `boolean`):

On: info@, sales@, support@ and similar shared inboxes get status "risky". Off: they are scored down but still counted as accepts\_mail.

## `deduplicate` (type: `boolean`):

Case-insensitive. Duplicates in a pasted list are the most common cause of an inflated bill.

## `onlyStatuses` (type: `array`):

Leave empty to keep every row. Pick a subset to get, for example, only the addresses worth mailing.

## `concurrency` (type: `integer`):

How many DNS queries run at once. 10 is polite and fast enough for tens of thousands of addresses because results are cached per domain.

## `dnsTimeoutSeconds` (type: `number`):

Per-query timeout. A query that times out produces status "unknown", never a false "invalid".

## `dnsResolvers` (type: `array`):

Resolver IPs to query. Defaults to Cloudflare, Google and Quad9. Use the single entry "system" to use the container's own resolver instead.

## Actor input object example

```json
{
  "emails": [
    "sales@stripe.com",
    "info@apify.com",
    "someone@gmial.com",
    "test@mailinator.com",
    "hello@this-domain-does-not-exist-9x8y7z.com"
  ],
  "domains": [],
  "checkSpfDmarc": true,
  "treatDisposableAsInvalid": true,
  "treatRoleAddressAsRisky": true,
  "deduplicate": true,
  "onlyStatuses": [],
  "concurrency": 10,
  "dnsTimeoutSeconds": 5,
  "dnsResolvers": [
    "1.1.1.1",
    "8.8.8.8",
    "9.9.9.9"
  ]
}
```

# Actor output Schema

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

One row per address: status, score, MX, SPF, DMARC and risk flags.

# 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": [
        "sales@stripe.com",
        "info@apify.com",
        "someone@gmial.com",
        "test@mailinator.com",
        "hello@this-domain-does-not-exist-9x8y7z.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("thedeadpoet/bulk-email-verifier-mx-spf-dmarc-disposable-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": [
        "sales@stripe.com",
        "info@apify.com",
        "someone@gmial.com",
        "test@mailinator.com",
        "hello@this-domain-does-not-exist-9x8y7z.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("thedeadpoet/bulk-email-verifier-mx-spf-dmarc-disposable-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": [
    "sales@stripe.com",
    "info@apify.com",
    "someone@gmial.com",
    "test@mailinator.com",
    "hello@this-domain-does-not-exist-9x8y7z.com"
  ]
}' |
apify call thedeadpoet/bulk-email-verifier-mx-spf-dmarc-disposable-check --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thedeadpoet/bulk-email-verifier-mx-spf-dmarc-disposable-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/W5MK60DCDYrHYIuo2/builds/NoDRl9ewesqwfz02Z/openapi.json
