# Bulk Email Verifier — Low-Bounce List Cleaner (`leadharbor/bulk-email-verifier`) Actor

Verify any list of emails before you send. Checks syntax, domain MX, disposable, role and free-provider — returns a valid / risky / invalid verdict per address so your bounce rate stays low.

- **URL**: https://apify.com/leadharbor/bulk-email-verifier.md
- **Developed by:** [Mohammed Samaan](https://apify.com/leadharbor) (community)
- **Categories:** E-commerce, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 email-verifieds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

Paste a list, chain a dataset, or point at a CSV URL — get back every address
with a status, a 0–100 score, and the exact reason, so you can delete the
bounces before they wreck your sender reputation.

### Who it's for

- Cold-email agencies and SDR teams cleaning a list before a send
- Anyone chaining this after a scraper (ours or another) to grade the emails
  it found before they hit an outreach tool
- Newsletter and e-commerce lists that need a periodic bounce-risk cleanup

### What you get

| Field | What it is |
|---|---|
| `email` | Normalized address |
| `status` | `valid` / `invalid` / `catch_all` / `unknown` / `disposable` / `risky` |
| `score` | 0–100 deliverability confidence |
| `reason` / `reason_detail` | Machine-readable code + a plain-English sentence |
| `syntax_valid` | Passed the offline format check |
| `mx_found` / `mx_host` | Domain has a mail destination, and which one |
| `smtp_checked` | Whether the mailbox-level check actually ran |
| `smtp_valid` | Mail server's verdict on the mailbox, `null` if not checked |
| `is_disposable` / `is_role` / `is_free_provider` | Throwaway / generic inbox / consumer provider |
| `has_tag` / `base_email` | `+tag` detection and the de-dupe base address |
| `typo_suggestion` | Suggested fix for a likely-mistyped domain, or `null` |
| `domain_has_spf` / `domain_has_dmarc` | Whether the domain publishes those records |
| `checked_at` / `duration_ms` | Timestamp and how long the check took |

A `SUMMARY` record with the run's tally is written to the key-value store.

### Input

```json
{
  "emails": ["info@apify.com", "someone@gmial.com", "throwaway@mailinator.com"],
  "treatRoleAsRisky": true,
  "concurrency": 20
}
```

Instead of (or alongside) `emails`, you can chain another actor's dataset with
`inputDatasetId` + `emailField`, or point `csvUrl` at a CSV file (Google Sheets
and Dropbox share links are converted to direct downloads automatically). All
three sources merge and de-duplicate in one run.

### Verification depth

Every address always gets: syntax check, typo suggestion, plus-tag detection,
disposable/role/free-provider lookup, MX lookup, and SPF/DMARC presence check
— all free, offline or DNS-only.

**SMTP mailbox check is available when a relay is configured.** Apify's cloud
blocks outbound port 25, so the mailbox-level `RCPT TO` probe runs through a
small relay you point the actor at (`relayUrl` + `relayToken`, or the
`EMAIL_RELAY_URL` / `EMAIL_RELAY_TOKEN` environment variables). A relay is
also what lets the actor detect catch-all domains, by probing a random address
on the domain first. **Without a relay, the run does not fail** — it logs a
warning, sets `smtp_checked: false` on every row, and grades from syntax,
lists, MX and domain health instead. The relay is open source and ships in
this repo under `email-relay/`.

### Pricing

**$0.50 per 1,000 emails verified.** Addresses that fail the free, offline
syntax check are not charged by default (`skipInvalidSyntax`). Duplicates are
removed before verification, so you pay for unique addresses only.

### How it compares

No browser, no per-second compute — verification is DNS lookups and (when a
relay is configured) short SMTP conversations that stop before `DATA`, so
nothing is ever sent. That keeps this fast and cheap to run against large
lists compared to a verifier built around a heavier scraping stack.

### Use with n8n / Make / Zapier

Add an Apify integration node, run this actor with your list or dataset ID as
input, then read the finished dataset back into your workflow to filter out
anything that isn't `valid` before it reaches your send step.

### FAQ

**Why is an address `unknown`?** Either the mail server greylisted the probe
and it didn't clear after retries, the server refuses probes from unknown IPs,
or no relay was configured for mailbox-level checking. `unknown` means the
check didn't reach a verdict — it isn't a soft "invalid".

**Does it detect catch-all domains?** Only when a relay is configured — that's
what lets the actor probe a random address on the domain to test whether it
accepts mail for anything. Without a relay, catch-all domains are not flagged;
they come back MX-only.

**Do you send email to the addresses?** No. The SMTP conversation, when it
runs, stops at `RCPT TO` and never reaches `DATA`.

**Can I re-run it later?** Yes — mailboxes go stale. Re-verifying a list every
few months is normal practice.

**What does the score mean?** A 0–100 deliverability confidence built from
whichever checks actually ran, adjusted for role/free-provider/typo/plus-tag
flags — not a claimed accuracy percentage, since accuracy depends on the mix
of domains in your list.

### More from leadharbor

- [Google Maps Verified Email Scraper](https://apify.com/leadharbor/google-maps-verified-email-scraper)
- [Airbnb Listings Scraper](https://apify.com/leadharbor/airbnb-listings-scraper)
- [Website Contact Extractor](https://apify.com/leadharbor/website-contact-extractor)
- [E-commerce Price & Restock Monitor](https://apify.com/leadharbor/ecommerce-price-restock-monitor)
- [App Store Review Monitor](https://apify.com/leadharbor/app-store-review-monitor)
- [Local Review Booster](https://apify.com/leadharbor/local-review-booster)

# Actor input Schema

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

Paste the email addresses to check, one per line. You can combine this with a dataset or a CSV URL — everything is merged and de-duplicated.

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

Dataset ID to pull emails from — e.g. the output of the Google Maps Verified Email Scraper. Leave blank if you are not chaining actors.

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

Which field of that dataset holds the email address.

## `csvUrl` (type: `string`):

A direct link to a CSV file. Google Sheets share links and Dropbox links are converted automatically — the sheet must be viewable by anyone with the link.

## `emailColumn` (type: `string`):

Exact header name of the email column. Leave blank to auto-detect (looks for common headers, then for the column that actually contains addresses).

## `checkSmtp` (type: `boolean`):

Ask the mail server whether the exact mailbox exists (RCPT TO) and detect catch-all domains. Requires a relay — without one the run is MX-only and says so on every row.

## `relayUrl` (type: `string`):

Base URL of the SMTP relay that performs the mailbox check (port 25 is blocked on Apify, so the probe runs on your own server). Leave blank to use the EMAIL\_RELAY\_URL environment variable.

## `relayToken` (type: `string`):

Bearer token for the relay. Leave blank to use the EMAIL\_RELAY\_TOKEN environment variable.

## `treatRoleAsRisky` (type: `boolean`):

Mark generic inboxes like info@, sales@ and support@ as risky. They are deliverable but rarely get a reply.

## `treatFreeProviderAsRisky` (type: `boolean`):

Mark gmail.com, yahoo.com and similar as risky. Off by default — most B2C lists are full of perfectly good free-provider addresses.

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

Malformed addresses are caught offline for free. Leave this on and you are not billed for them.

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

How many addresses to check at once. Probes against any single mail server are capped at 3 regardless, so raising this mainly speeds up mixed-domain lists.

## `timeoutSecs` (type: `integer`):

How long to wait on a DNS lookup or a mail server before giving up and returning 'unknown'.

## `maxRetries` (type: `integer`):

Greylisting (a 4xx 'try again later' from the mail server) is retried this many times with backoff before the address is marked 'unknown'.

## `maxEmails` (type: `integer`):

Hard cap on how many addresses one run will verify. Protects you from an accidentally huge input.

## Actor input object example

```json
{
  "emails": [
    "info@apify.com",
    "someone@gmial.com",
    "throwaway@mailinator.com",
    "nobody@thisdomaindoesnotexist1234567.com"
  ],
  "emailField": "email",
  "checkSmtp": true,
  "treatRoleAsRisky": true,
  "treatFreeProviderAsRisky": false,
  "skipInvalidSyntax": true,
  "concurrency": 20,
  "timeoutSecs": 10,
  "maxRetries": 2,
  "maxEmails": 50000
}
```

# Actor output Schema

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

One row per email: status, score, reason, typo suggestion, domain health.

## `resultsUi` (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": [
        "info@apify.com",
        "someone@gmial.com",
        "throwaway@mailinator.com",
        "nobody@thisdomaindoesnotexist1234567.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("leadharbor/bulk-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": [
        "info@apify.com",
        "someone@gmial.com",
        "throwaway@mailinator.com",
        "nobody@thisdomaindoesnotexist1234567.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("leadharbor/bulk-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": [
    "info@apify.com",
    "someone@gmial.com",
    "throwaway@mailinator.com",
    "nobody@thisdomaindoesnotexist1234567.com"
  ]
}' |
apify call leadharbor/bulk-email-verifier --silent --output-dataset

```

## MCP server setup

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