# Email Finder Bulk — Lists of Domains or People to Emails (`bovi/email-finder-bulk`) Actor

Find published contact emails from company websites and work-email candidates from names and company domains. Get source, confidence, status, and role fields. Run with domains or people input; no API key or login. Pricing: $0.0049 per email-found event.

- **URL**: https://apify.com/bovi/email-finder-bulk.md
- **Developed by:** [Vitalii Bondarev](https://apify.com/bovi) (community)
- **Categories:** Lead generation, Business
- **Features:** Limited permissions
- **Stats:** 176 total users, 11 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.75 / 1,000 email founds

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?

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

## Bulk Email Finder for Domains and People

Email Finder Bulk uses Mode A (`people`) to find work-email candidates from a name and company domain, and Mode B (`domains`) to harvest published emails from company websites. Free-plan accounts process 10 people and 3 domains per run, up to 2 pages per domain, at $0.0049 per `email-found` event on Free.

- Harvest emails from contact pages, `mailto` links, and page footers.
- Return a best candidate with ranked alternatives, source, and confidence.
- Build outreach lists, enrich CRM records, and research leads.
- No third-party API key or website login is needed; runs start from your Apify account. Cross-list deduplication is enabled by default.
- Mode A tries SMTP verification and falls back to a ranked `unverified_guess` when port 25 is unavailable or `verifySmtp` is `false`.

### What you get

The Actor returns one flat dataset row per email. A field is `null` when its source does not provide it.

| Field | Type | What it contains | Mode or values |
| --- | --- | --- | --- |
| `domain` | string | Mode A normalized domain; Mode B supplied entry, trimmed and lowercased, so a URL remains a URL. Never `null` or empty | Both |
| `email` | string | Found or generated address; never `null` or empty | Both |
| `first_name` | string | Associated first name; `null` for Mode B | People; `null` for Mode B |
| `last_name` | string | Last name, or `null` when absent, not derived, or Mode B | People or `null` |
| `email_source` | string | How the email was reached; never `null` or empty | `mailto`, `contact_page`, `website_body`, `name_pattern`, `smtp_verified` |
| `confidence` | number | Score from 0 to 1. Mode A: `verified` 0.90–1.0, `accept_all` ≤0.55, `unverified_guess` ≤0.45, `no_valid_pattern` ≤0.10, `no_mx` ≤0.05. Never `null` | Both |
| `duplicate_flag` | boolean | Whether a repeat is emitted when deduplication is disabled | `true` or `false` |
| `status` | string | Result state; never `null` or empty | `verified`, `accept_all`, `unverified_guess`, `no_valid_pattern`, `no_mx`, `harvested` |
| `is_role_account` | boolean | Whether the address is shared, such as `info@` or `support@` | Both |
| `is_disposable` | boolean | Disposable-address flag | Both |
| `mx_found` | boolean | Whether an MX record was found; `null` for harvested rows | Mode A boolean; Mode B `null` |
| `verification_method` | string | Verification approach | `smtp` for `verified`, `accept_all`, `no_valid_pattern`; `mx_pattern` for `unverified_guess`; `none` for Mode B and `no_mx` |
| `on_domain` | boolean | Whether the email domain matches the site domain | Both |
| `alternative_emails` | array of objects | Ranked person candidates; empty for Mode B, or up to `maxAlternatives` for Mode A | `email`, `pattern`, `confidence`, `smtpStatus`, `smtpCode` |
| `parse_confidence` | number | Acquisition reliability. Mode B: 0.95 `mailto`, 0.85 `contact_page`, 0.70 `website_body`. Mode A: 1.0, 0.9 when port 25 cannot connect, 0.5 on `no_mx`. Never `null` | Both |
| `input_mode` | string | Mode that produced the row | `people` or `domains` |
| `scraped_at` | ISO 8601 string | UTC run-processing timestamp, shared by all rows | Timestamp string |

### Sample output

Example dataset record (Mode B, `input_mode` `domains`). Each charged row is one `email-found` event; see Pricing.

```json
{
  "domain": "diverxo.com", "email": "info\u0040diverxo.com",
  "first_name": null, "last_name": null,
  "email_source": "mailto", "confidence": 0.92,
  "duplicate_flag": false, "status": "harvested",
  "is_role_account": true, "is_disposable": false,
  "mx_found": null, "verification_method": "none",
  "on_domain": true, "alternative_emails": [],
  "parse_confidence": 0.95, "input_mode": "domains",
  "scraped_at": "2026-10-30T01:23:32Z"
}
```

### Pricing

- Mode A: one `email-found` event per row with an email and status `verified`, `accept_all`, `unverified_guess`, `no_valid_pattern`, or `no_mx`, including guesses.
- Mode B: one event per row with `confidence` of at least 0.5; harvested rows score 0.59–0.95.
- Rows without an email and repeats collapsed while `deduplicateEmails` is `true` are not charged.
- With `deduplicateEmails` set to `false`, repeats are kept with `duplicate_flag=true` and charged.

You pay only for the events below. This Actor does not use Pay per event + usage, so platform usage for runs (compute, proxy traffic, and storage) is not billed separately.

| Plan | Price |
| --- | ---: |
| Free | $0.0049 per `email-found` event |
| Bronze | $0.004851 per `email-found` event |
| Silver | $0.004802 per `email-found` event |
| Gold | $0.004753 per `email-found` event |
| Platinum | $0.004704 per `email-found` event |
| Diamond | $0.004655 per `email-found` event |

Examples: 1,000 results cost $4.90 on Free; 10,000 results cost $49.00 on Free.

A Free-plan run is capped at 10 people and 3 domains, so totals like these take several runs.

The Apify Free plan includes $5 of monthly platform usage credit; the $5 credit covers 1,020 `email-found` events at $0.0049 per `email-found` event on Free ($4.998 on Free).

### How to use

1. Prepare a `domains` list, a `people` list, or both.
2. Add options for deduplication, domain limits, SMTP verification, alternatives, and output limits.
3. Configure `proxyConfiguration` for Mode B; the default uses Apify Proxy with the `RESIDENTIAL` group.
4. Start the Actor run from your Apify account.
5. Review `email_source`, `confidence`, `status`, and `verification_method`.
6. Repeat the run through Apify Schedules; weekly is a reasonable starting point for refreshing lead lists.

```json
{
  "domains": ["stripe.com", "notion.so"],
  "people": [{"firstName": "Patrick", "lastName": "Collison", "domain": "stripe.com"}],
  "deduplicateEmails": true,
  "maxPerDomain": 3,
  "verifySmtp": true,
  "maxAlternatives": 5,
  "maxItems": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"]
  }
}
```

Export the dataset as JSON or CSV, or read it through the Apify dataset API.

### Input parameters

| Parameter | What it does | Default |
| --- | --- | --- |
| `domains` | Mode B domains or website URLs for harvesting published emails | — |
| `people` | Mode A people; each needs `domain` and `firstName` or `fullName`; `lastName` or `surname` is optional | — |
| `deduplicateEmails` | Collapses the same email across inputs; repeats receive `duplicate_flag=true` when disabled | `true` |
| `maxPerDomain` | Caps harvested emails per domain; integer from 1 to 50 | `3` |
| `verifySmtp` | Attempts live SMTP verification in Mode A; otherwise uses MX and pattern ranking | `true` |
| `maxAlternatives` | Ranked candidates per person; integer from 0 to 12 | `5` |
| `smtpTimeoutSecs` | Timeout for each SMTP dialog; integer from 2 to 20 seconds. The TCP connect to port 25 is capped at 2 seconds, and once two mail servers cannot be reached the run stops trying SMTP and uses MX and pattern ranking | `5` |
| `maxItems` | Total output cap; `0` means no limit | `0` |
| `proxyConfiguration` | Proxy used to reach Mode B websites | Apify Proxy with `RESIDENTIAL` |
| `disableUsageStats` | Turns off the run summary described in Usage statistics when `true` | `false` |

Values outside the ranges for `maxPerDomain`, `maxAlternatives`, or `smtpTimeoutSecs` are clamped to the nearest bound; the run does not fail.

### Limits and edge cases

| Condition | What you see | What to do |
| --- | --- | --- |
| Neither mode has valid input | The `no_input` run writes `OUTPUT` `{"error":"no_input"}`, ends as Succeeded, and has an empty dataset | Provide valid input |
| A Mode A entry lacks `domain` or a name, or a Mode B domain is empty or has no dot | The entry is dropped; the log shows `Dropped N invalid people` or `Dropped N invalid domains` | Supply valid input |
| A Mode A `domain` has no dot | The entry is not dropped at input. It returns `invalid_input` with no email, no row, no charge, and no warning; `skipped` in `OUTPUT` includes it | Use a dotted domain |
| More than 5,000 people or domains are supplied | Each list is capped at 5,000; the cap is applied first | Split the workload |
| Free plan receives more than 10 people or 3 domains | Only the first 10 people and 3 domains are processed | Keep within the limits |
| A harvested domain has no eligible publicly available email | No row or charge | Check the domain |
| A use involves publicly available contact information | The Actor extracts public data from company websites and uses domain mail records for Mode A checks | Check the target site's ToS and applicable law for your use case |
| A Mode B page fetch fails or is blocked | The page is skipped and the run continues. If no fetched page has an eligible email, the log shows `<domain>: no emails found.` and the run still ends as Succeeded. If the homepage fails, no other page is tried. The reason is not recorded; `OUTPUT` contains counts only | — |
| SMTP is enabled but port 25 is unavailable, or `verifySmtp=false` | Mode A returns `unverified_guess` with `mx_pattern`, confidence at most 0.45, and one charged event | Check `alternative_emails` |
| A Mode A domain accepts any address | Returns `accept_all`, confidence at most 0.55, `verification_method` `smtp`, and one charged event | Use `alternative_emails` |
| SMTP works but no candidate is accepted | Returns `no_valid_pattern`, confidence at most 0.10, `verification_method` `smtp`, and one charged event | Use `alternative_emails` |
| A person's domain has no resolvable mail server | Returns a charged `no_mx` row with the top pattern as `email`; no MX and no A record was found, or DNS failed or timed out | Check the domain |
| A normal run ends | `OUTPUT` contains only `totalItems`, `peopleProcessed`, `domainsProcessed`, `duplicatesCollapsed`, `skipped`, and `scrapedAt` | Read the dataset |
| Rows are saved during a run | Rows reach the dataset in batches of 100 and once more at the end; each batch is charged after it is pushed | Read saved batches |
| A run is aborted, times out, or fails after rows are pushed | Pushed rows remain. Up to 99 rows waiting in the current batch are not saved or charged, and `OUTPUT` is not written | Check saved batches |
| Mode B runs on the Free plan | Up to 2 pages per domain are used; paying accounts can use up to 4. `maxPerDomain` still limits returned rows | Adjust the account or split the workload |

### FAQ

#### How much does it cost to scrape company domains?

Free costs $0.0049 per `email-found` event; 1,000 results cost $4.90 on Free. See Pricing for all plan-labelled rates.

#### What happens when no email is found?

A Mode B domain with no published email produces no row and no charge. A Mode A person whose domain has no resolvable mail server gets a charged `no_mx` row with the top-ranked pattern as `email`.

#### Why might SMTP verification not work?

Mode A falls back to MX and pattern ranking when outbound port 25 is unavailable or `verifySmtp` is `false`; see Limits and edge cases.

#### Is it legal to use this public data?

The Actor extracts publicly available contact information published on company websites and uses domain mail records for Mode A checks. Whether a use is legal or allowed depends on applicable law, the target site's ToS, and your use case.

### Related actors

- [Email Finder — Name + Domain to Work Email + Score](https://apify.com/bovi/email-finder) — related single-person and domain work-email finder.
- [Bulk Email Verifier — Validate Emails, MX, Disposable & Role](https://apify.com/bovi/bulk-email-verifier) — related bulk email, MX, disposable, and role validation tool.
- [Email Verifier — Bulk SMTP & MX Validation, Catch-all](https://apify.com/bovi/email-verifier) — related bulk SMTP, MX, and catch-all validation Actor.

### Usage statistics

This Actor creates a small, content-free summary at the end of each run, used only to monitor reliability and improve this Actor. No input values, credentials or scraped data are sent.

A copy of the exact record for your run is saved as `USAGE_STATS` in your own Apify key-value store.

To opt out, set `disableUsageStats` to `true` in the input. Nothing is sent then.

[Full list of collected fields](https://api.apify.com/v2/key-value-stores/v9rgHjMKLDGtHf8tD/records/index.html)

# Actor input Schema

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

List of company domains or website URLs to harvest published emails from (contact pages, mailto links, page footers). No names needed. Example: \["stripe.com", "notion.so"].

## `people` (type: `array`):

List of people to find work emails for. Each item: firstName (required), lastName (or surname), optional fullName, and domain (required). Example: {"firstName": "Patrick", "lastName": "Collison", "domain": "stripe.com"}.

## `deduplicateEmails` (type: `boolean`):

Collapse the same email found via two inputs into a single row. When off, every occurrence is kept and repeats are marked with duplicate_flag=true. Default: true.

## `maxPerDomain` (type: `integer`):

Cap how many emails to return per harvested domain — controls cost and noise. Default: 3.

## `verifySmtp` (type: `boolean`):

For the people mode: attempt live SMTP RCPT verification so confirmed mailboxes are marked 'verified'. If your run network blocks outbound port 25, it falls back to MX + pattern ranking. Default: true.

## `maxAlternatives` (type: `integer`):

How many ranked alternative-email candidates to return per person (each with its own confidence). Default: 5.

## `smtpTimeoutSecs` (type: `integer`):

Timeout for each SMTP connection attempt. Increase on slow mail servers. Default: 5.

## `maxItems` (type: `integer`):

Maximum total records to output across both modes. 0 = no limit. Useful for cost control. Default: 0.

## `proxyConfiguration` (type: `object`):

Proxy used to reach websites in Mode B. Residential is the default and recommended — sites are never touched from a bare datacenter IP.

## `disableUsageStats` (type: `boolean`):

See the README Usage statistics section for details.

## Actor input object example

```json
{
  "domains": [
    "diverxo.com",
    "asadoretxebarri.com"
  ],
  "people": [
    {
      "firstName": "Patrick",
      "lastName": "Collison",
      "domain": "stripe.com"
    }
  ],
  "deduplicateEmails": true,
  "maxPerDomain": 3,
  "verifySmtp": true,
  "maxAlternatives": 5,
  "smtpTimeoutSecs": 5,
  "maxItems": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "disableUsageStats": false
}
```

# Actor output Schema

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

Dataset of email rows (domain, email, first_name, last_name, email_source, confidence, duplicate_flag, status, is_role_account, on_domain, parse_confidence).

# 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 = {
    "domains": [
        "diverxo.com",
        "asadoretxebarri.com"
    ],
    "people": [
        {
            "firstName": "Patrick",
            "lastName": "Collison",
            "domain": "stripe.com"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("bovi/email-finder-bulk").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 = {
    "domains": [
        "diverxo.com",
        "asadoretxebarri.com",
    ],
    "people": [{
            "firstName": "Patrick",
            "lastName": "Collison",
            "domain": "stripe.com",
        }],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("bovi/email-finder-bulk").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 '{
  "domains": [
    "diverxo.com",
    "asadoretxebarri.com"
  ],
  "people": [
    {
      "firstName": "Patrick",
      "lastName": "Collison",
      "domain": "stripe.com"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call bovi/email-finder-bulk --silent --output-dataset

```

## MCP server setup

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

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/7RlYD6BGyZXei4hLd/builds/d708p8EjkRNMt8tpE/openapi.json
