# Email Verifier - Honest Deliverability Check (`garnet_puppet/email-verifier`) Actor

Give it a list of email addresses and it reports what is provably wrong with them, what looks risky, and what could not be determined. It never claims a mailbox exists, because from this platform that cannot be checked - and it says so in every row.

- **URL**: https://apify.com/garnet\_puppet/email-verifier.md
- **Developed by:** [Mehmet](https://apify.com/garnet_puppet) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.68 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

Give this Actor a list of email addresses and it tells you what is provably wrong with
them, what looks risky, and — just as clearly — what it could not determine. One row of
output is one address.

### The honest part, up front

Real mailbox verification requires an SMTP conversation with the receiving mail server
(`RCPT TO`). **Port 25 is closed on the Apify platform.** We measured it twice: once in
an earlier run on the platform, and again while building this Actor.

So this Actor **never tells you an address is valid**, because it cannot know. Every
record carries two fixed fields that say so:

```json
"smtpChecked": false,
"mailboxVerified": null
```

The best verdict it will ever give is `deliverable_domain` — *the domain accepts mail,
the mailbox is unknown*.

If you have seen another Apify email verifier return `"smtp_valid": true` or
`"reason": "SMTP mailbox exists"`, that claim cannot be produced from inside this
platform. We would rather lose the comparison than repeat it.

### What it does check

| Check | What it proves |
|---|---|
| **Syntax** | Malformed addresses. Definitive. |
| **MX records** | Domain has no mail server → mail cannot be delivered. Definitive. |
| **Disposable providers** | Temp-mail services, checked before MX because their MX records usually work fine. |
| **Role accounts** | `info@`, `support@`, `sales@` — flagged, never dropped. |
| **Placeholder locals** | `someone@`, `test@`, `yourname@` — template-looking addresses, flagged, never dropped. |
| **Free providers** | Gmail, Outlook, Yahoo and friends. |
| **Tagged addresses** | `john+newsletter@` — the tag is separated out. |
| **Typos** | `gmial.com` → `gmail.com`. |
| **Mail provider** | Google Workspace, Microsoft 365, Zoho, Proton… |

That last one is not in any competing Actor's output, and it costs nothing extra: the
provider is already inside the MX records we fetched.

### Status values

| Status | Meaning |
|---|---|
| `invalid_syntax` | Not an email address. Certain. |
| `no_mx` | The domain has no mail server. Certain: mail cannot be delivered. |
| `disposable` | A temporary-mail provider. |
| `deliverable_domain` | The domain accepts mail. **The mailbox was not checked.** |
| `unknown` | The DNS lookup failed, or MX checking was turned off. No information. |

`unknown` matters more than it looks. A DNS timeout is **not** the same as "no mail
server", and treating it as one would make you delete a working address. Those two
outcomes are kept apart on purpose.

### Score

Every record has a `score` from 0 to 100. It is a **judgement, not a measurement** — a
convenience for sorting. The `reasons` array is the honest version: it lists every
individual finding (`syntax_ok`, `mx_ok`, `role_account`, `possible_typo`,
`disposable_provider`, `mx_lookup_failed`, …) so you can apply your own rules.

### Input

| Field | Default | What it does |
|---|---|---|
| `emails` | — | The addresses to check |
| `inputDatasetId` | — | Read addresses from another Actor's dataset instead |
| `emailField` | `email` | Which field holds the address (dotted paths work) |
| `checkMx` | `true` | Look up mail servers. One DNS query per domain, cached |
| `suggestTypos` | `true` | Suggest fixes for near-misses of common providers |
| `skipInvalidSyntax` | `false` | Leave malformed addresses out of the results |
| `incremental` | `false` | Skip addresses this task already returned |
| `previewOnly` | `false` | Report what would be billed, check nothing |

`inputDatasetId` chains this Actor onto anything that produces emails — including a
scraper run from an earlier step in the same workflow.

### Output

```json
{
  "email": "john@stripe.com",
  "input": "  John@Stripe.COM  ",
  "normalized": true,
  "domain": "stripe.com",
  "localPart": "john",
  "tag": null,
  "status": "deliverable_domain",
  "score": 85,
  "reason": "domain_accepts_mail",
  "reasons": ["syntax_ok", "mx_ok", "mx_provider_known"],
  "syntax_valid": true,
  "domain_exists": true,
  "mx_found": true,
  "mx_records": ["aspmx.l.google.com"],
  "mxProvider": "Google Workspace",
  "is_disposable": false,
  "is_role_based": false,
  "is_free_provider": false,
  "is_placeholder": false,
  "has_tag": false,
  "did_you_mean": null,
  "smtpChecked": false,
  "mailboxVerified": null,
  "verified_at": "2026-08-16T12:00:00.000Z",
  "contentHash": "…"
}
```

Field names in the first group match the widely used email verifiers on Apify, so an
existing spreadsheet, Make scenario or Zapier step keeps working. The extra fields are
additions, not replacements.

### Billing

One event: **`email-verified`**, charged per delivered record.

Never billed:

- malformed addresses (syntax is checked before anything is charged)
- addresses skipped by incremental mode
- anything at all in preview mode

### Speed and cost

MX lookups are one DNS query **per domain**, cached for the whole run — a list of 1,000
addresses across 150 domains does 150 lookups, not 1,000. On the platform a lookup takes
single-digit milliseconds. No proxy, no browser, nothing is scraped.

### Frequently asked

**Why won't it just say valid or invalid?**
Because from inside this platform that would be a guess wearing a confident label. What
can be proven is proven; what cannot is reported as unknown. You get the underlying
findings and decide your own threshold.

**Can it detect catch-all domains?**
No. That requires SMTP, so instead of returning a fabricated `catch_all` field, it
returns none.

**Is `no_mx` reliable enough to delete an address?**
Yes — that is the one negative this Actor is confident about. A domain with no MX record
cannot receive mail. Note it is distinct from `unknown`, which means the lookup itself
did not succeed.

**Does it send email?**
No. Nothing is sent, and no mailbox is contacted.

**Why is `test@mycompany.com` not reported as malformed?**
Because it isn't. It looks like a placeholder, and that is worth knowing, so it comes
back with `is_placeholder: true` — but calling valid syntax "invalid" would make you
delete an address that may well be someone's real inbox. Flags describe; they do not
decide for you.

**What does `mxProvider` distinguish?**
Consumer mailboxes from business ones. `gmail.com` resolves to Gmail's consumer servers
and is reported as `Gmail`; a company on Google Workspace resolves to different servers
and is reported as `Google Workspace`. Same company, different products, and for
outreach the difference matters.

# Actor input Schema

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

The addresses to check. One per line.

## `inputDatasetId` (type: `string`):

ID of a dataset produced by another Actor. Addresses are read from the field named below, so you can chain this straight onto a scraper that finds emails.

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

Which field of each dataset item holds the email address. Dotted paths work, for example aboutChannelInfo.email.

## `checkMx` (type: `boolean`):

Look up each domain's mail servers. This is the single most useful check and it is fast: one DNS query per domain, cached, typically 2-9 ms. Turn it off for a syntax-only pass.

## `suggestTypos` (type: `boolean`):

Spot near-misses of common providers, such as gmial.com for gmail.com. Deliberately conservative: it will not second-guess a domain that looks real.

## `skipInvalidSyntax` (type: `boolean`):

Leave addresses that are not valid email syntax out of the results entirely. They are never billed either way; this only keeps your dataset clean.

## `incremental` (type: `boolean`):

Remembers addresses this task already returned and skips them, so a scheduled run does not bill you twice for the same address.

## `previewOnly` (type: `boolean`):

Report how many addresses would be checked and billed, without checking or billing any of them. The platform's own Actor start event still applies.

## Actor input object example

```json
{
  "emails": [
    "john@stripe.com",
    "support@gmail.com",
    "test@mailinator.com",
    "someone@gmial.com"
  ],
  "emailField": "email",
  "checkMx": true,
  "suggestTypos": true,
  "skipInvalidSyntax": false,
  "incremental": false,
  "previewOnly": false
}
```

# Actor output Schema

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

One item per address: status, score, the individual findings behind it, mail provider and typo suggestion.

## `runSummary` (type: `string`):

Counts for the whole run: what was delivered, what was skipped, what failed, and any warnings. Read this to see whether the run did what you expected.

# 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": [
        "john@stripe.com",
        "support@gmail.com",
        "test@mailinator.com",
        "someone@gmial.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("garnet_puppet/email-verifier").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": [
        "john@stripe.com",
        "support@gmail.com",
        "test@mailinator.com",
        "someone@gmial.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("garnet_puppet/email-verifier").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": [
    "john@stripe.com",
    "support@gmail.com",
    "test@mailinator.com",
    "someone@gmial.com"
  ]
}' |
apify call garnet_puppet/email-verifier --silent --output-dataset

```

## MCP server setup

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

```

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/PNQoeAMDCSiKkZCI2/builds/adKq1aP5BEi8hJPGX/openapi.json
