# Bulk Email Validator - Clean Your List Before You Send (`salomon.labs/bulk-email-validator`) Actor

Remove invalid, dead-domain, disposable and role addresses from any email list, and get a 0-100 send-risk score with a plain-English reason for every row.

- **URL**: https://apify.com/salomon.labs/bulk-email-validator.md
- **Developed by:** [Alan Salomon](https://apify.com/salomon.labs) (community)
- **Categories:** Lead generation, Automation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 validated emails

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/platform/actors/running/actors-in-store#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 Validator — clean your email list before you send

Paste a list, link a CSV, or chain it after a scraper. Every address comes back
**validated, classified and scored**, so the junk is gone before it ever touches
your sending domain.

This Actor removes the addresses that are provably bad and flags the ones that are
risky to send to — **without sending anything and without needing an API key**.

### What it removes and flags

| Check | What it catches |
|---|---|
| **Syntax validation** | Malformed addresses, typos, broken exports |
| **Domain & MX check** | Domains that don't exist or can't receive mail at all — usually the biggest slice of a scraped list |
| **Disposable detection** | Temporary and burner mailboxes |
| **Role account detection** | `info@`, `sales@`, `support@`, `admin@` — deliverable, but rarely a person |
| **Free provider detection** | Gmail, Outlook, Yahoo and friends, flagged so you can filter a B2B list |
| **Mail provider identification** | Google Workspace, Microsoft 365, and other hosts |
| **Send-risk scoring** | A 0–100 score and a recommended action, with the reason spelled out |

### What you get per address

| Column | What it's for |
|---|---|
| `safe_to_send_score` | 0–100. The one number to branch on. |
| `recommended_action` | `send`, `send_low_volume`, `secondary_verification`, `retry_later`, `manual_review`, `suppress` |
| `recommendation_detail` | One plain sentence explaining the decision |
| `status` | The verdict: `deliverable`, `undeliverable`, `risky`, `unknown` |
| `is_disposable`, `is_role_account`, `is_free_provider` | Filter your list however you like |
| `mail_provider` | Who actually runs the mailbox |
| `confidence_score` | How sure we are of our own assessment |
| `reason_codes` | Exactly which signals produced the decision |

Every run's dataset ships with two ready-made views. **Decisions** is the one you
want for a clean spreadsheet — export CSV from that tab and you get eight columns
in the order you actually read them. **Evidence** shows the signals behind each
call. Exporting the unfiltered dataset gives you every column, sorted
alphabetically — useful for analysis, less pleasant to read.

### Be clear about what this does and does not check

**It does not connect to mail servers to confirm that an individual mailbox
exists.** No SMTP probing, no mailbox-level verification.

That is deliberate, and it is why this is cheap and fast. What it does instead is
remove everything that can be proven bad without touching a mail server — and on a
typical scraped or purchased list that is a large share of the file.

**Use it as the first pass.** Clean here, then, if you need mailbox-level
confirmation on what survives, send only the survivors to a per-email SMTP
verifier. You pay the expensive per-email price on a much smaller list.

And it will never tell you a **catch-all** mailbox exists. Nobody can — a catch-all
domain accepts every address by design, so no verifier on the market can confirm
one, whatever the marketing says. Catch-all domains are reported honestly as
unresolved rather than guessed at.

**Honest uncertainty is the point.** A validator that guesses confidently is how
sending domains get burned.

#### What that means for your scores

Because no mailbox is contacted, an address that passes every check is reported as
**not disproven**, not as confirmed. In practice:

| What you'll see | What it means |
|---|---|
| `suppress` | **Provably bad** — broken syntax, dead domain, or disposable. Delete these with confidence; this is the bulk of what the Actor removes. |
| `send_low_volume` | **Passed every check available.** The domain accepts mail and nothing looks wrong. The mailbox itself was not probe-confirmed, so send carefully and watch bounces. Free mailboxes land here too — deliverable, just a weaker target on a B2B list. |
| `manual_review` | A shared role inbox (`info@`, `sales@`), or an address we could not process. A human should decide; never treated as bad. |
| `secondary_verification` | Genuinely uncertain — the domain resolves but gave no recognisable mail host. If you need certainty, this is the shortlist to send to a per-email SMTP verifier. |

**Branch on `recommended_action`, not on the number.** Without a mailbox check the
confidence ceiling applies to every address, so scores cluster low by design and
nothing reaches the 80s or 90s. The score is a useful ranking *within* a band; the
action is the decision.

### Input

Three ways in, and you can combine them — everything is merged and de-duplicated
before validation, so you are never charged twice for the same address:

- **Paste addresses** into the Email addresses field.
- **Link a CSV** (published Google Sheet, S3 link, anything downloadable). The
  email column is detected automatically, whatever it is called.
- **Chain a dataset** from a previous Actor — put its dataset ID in
  `inputDatasetId` and this runs straight after your scraper or enrichment step.

Messy input is expected and handled: display names, `mailto:` prefixes, BOMs,
semicolon delimiters, headerless files, and any column name.

### How the scoring works

Two numbers underpin the headline score, and they are deliberately separate:

- **Deliverability** — will a message be accepted?
- **Confidence** — how sure are we of that assessment?

`safe_to_send_score` combines them, then subtracts the outreach-specific risk that
deliverability alone ignores: role inboxes and free mailboxes on a B2B list are
perfectly deliverable and often a bad send. The score is also capped by
confidence — it will never claim an address is safer to send than we are sure of
our own answer.

Every score is explainable. `reason_codes` tells you exactly which signals produced
it, so you can argue with the result instead of trusting it blindly.

### One bad row never breaks your run

An address we cannot process comes back as a row marked `manual_review` and
`retryable`, with the same columns as every other row — never dropped, never
silently marked unsendable. Our failure is not your lead's failure, and a CSV whose
shape changes halfway down breaks every formula built on it.

### Typical uses

- **Clean a scraped or purchased list** before it touches your sending domain.
- **Cut the cost of email verification** — remove the provable junk here, pay
  per-email only for what survives.
- **Run it as the last step of an Apify pipeline** so leads land already scored.
- **Branch in n8n, Make or Zapier** on `recommended_action`.
- **Filter B2B lists** on `is_free_provider` and `is_role_account`.

### Pricing

Pay per address validated. Duplicates are removed before validation, so you are
never charged twice for the same address in a run.

# Actor input Schema

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

The addresses to validate. Duplicates are removed first, so you are never charged twice for the same address.

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

A direct link to a CSV file of leads - for example a published Google Sheet or an S3 link. The email column is detected automatically.

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

Read leads from the dataset of a previous Actor run - the usual way to chain this after a scraper or enrichment Actor.

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

Only needed if automatic detection picks the wrong column. Example: 'work\_email'.

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

How many addresses to validate at once. Raise it for large lists; lower it if you are being rate-limited.

## `mode` (type: `string`):

Leave this on Standard. Deep mode only does more work when this Actor is self-hosted with an SMTP worker attached; here on Apify it behaves exactly like Standard and costs the same.

## `allowSmtpProbe` (type: `boolean`):

Has no effect on Apify, which blocks outbound mail ports - this Actor does no mailbox-level probing here. The flag exists for self-hosted deployments that attach an SMTP worker. No message is ever sent in any deployment.

## Actor input object example

```json
{
  "emails": [
    "jane.doe@example.com"
  ],
  "concurrency": 10,
  "mode": "standard",
  "allowSmtpProbe": true
}
```

# Actor output Schema

## `decisions` (type: `string`):

The clean export: email, send-risk score, recommended action, the reason, status, disposable and role flags, and confidence - in that order. This is the one to use for a spreadsheet or a CSV download.

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

Every column for every address, including the evidence: mail provider, DNS and MX signals, individual reason codes, and the engine, score and policy versions that produced the row.

## `evidence` (type: `string`):

The signals behind each decision: domain, mail provider, role, free and disposable flags, domain behaviour and reason codes.

## `summary` (type: `string`):

Counts by recommended action and status for the whole run, whether SMTP probing was available, and whether the run stopped early on a spend limit.

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("salomon.labs/bulk-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"] }

# Run the Actor and wait for it to finish
run = client.actor("salomon.labs/bulk-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"
  ]
}' |
apify call salomon.labs/bulk-email-validator --silent --output-dataset

```

## MCP server setup

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