# Bulk Email Finder from CSV | No Cookies (`b2bdata/bulk-email-finder`) Actor

Bulk email finder for CSV lists: paste rows, map your columns (first name, last name, domain), get one work email per person with status and confidence. Only addresses on the company domain are returned. Free plan: 100 rows per run. Pay per email found, misses free. No login or cookies.

- **URL**: https://apify.com/b2bdata/bulk-email-finder.md
- **Developed by:** [Open Enrich](https://apify.com/b2bdata) (community)
- **Categories:** Lead generation, Automation
- **Features:** Limited permissions
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 email founds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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 from CSV | No Cookies

Bulk email finder for CSV lists: give your rows (first name, last name, company domain) and tell the actor which column is which, and get one work email per found person back, with a validity status and a 0-1 confidence score. Every result carries its `rowIndex`, so it joins straight back onto your spreadsheet. No login, no cookies, no start fee. You pay only for each email found.

**No login | CSV rows + column map in, work emails out | rowIndex to join back | Status + confidence per row | Up to 5,000 rows per run | Misses are free | JSON/CSV export**

> **Not an email verifier.** This actor finds one work email per person. It does not check lists of emails you already have. Run your own deliverability check before a campaign.

[See all actors by b2bdata](https://apify.com/b2bdata)

***

### 💳 Pricing and plans

One event, `email-found`, charged once per row delivered. Prices per 1,000 emails found (set on the Pricing tab):

| Event | When it is charged | Free plan | Gold plan |
|---|---|---|---|
| `email-found` | A work email on the domain in your row is found and the result row is delivered | $0.963 | $0.696 |

Bronze and Silver prices are on the Pricing tab. Higher Apify plans only lower the price. Empty runs, rows without a result, failed lookups, unreadable rows and notices are free, and there is no start fee.

**Plan-credit math.** The free plan's $5 monthly credit covers about 5,190 emails at the Free price.

#### Success-only billing

You are charged only for a result row you receive. A person who is not found, a row that cannot be read, a lookup that fails, or an address that is not on the domain you gave is listed in `MISSES` with its `rowIndex` and a reason and costs nothing.

#### The free plan

Free Apify accounts get up to 100 rows looked up per run, and as many runs as their Apify credit covers. Nothing is stored on our side. When a list is longer, the first 100 valid rows are looked up and one free notice row (`reason: "free_plan_limit"`) says how many were not looked up; they are not charged. Run again with the rest, or use a paid Apify plan for no per-run limit.

#### Keep a run small while you test

Use `limit` to cap the number of rows looked up. The default is 100, the maximum is 5,000, `0` means all valid rows up to 5,000. Try 1 to 3 rows first, read the result, then scale up.

***

### ⚡ Why this bulk email finder?

- **Your columns, your names.** `columnMap` points first name, last name, full name, domain, company name and LinkedIn URL at whatever your CSV calls them. Leave a key out and common headers (`First Name`, `Surname`, `Website`, `Company Domain`) are detected.
- **Joins back to your CSV.** Each result has `rowIndex` (0 = your first row). Each row without a result is in `MISSES` with the same index and a reason.
- **Only addresses on your domain.** An address on any other domain is never returned as the result.
- **Status and confidence on every row.** `emailStatus` is `valid`, `catch_all` or `unknown`; `invalid` addresses are not delivered. `confidence` follows a fixed rule you can read below.
- **Success-only, no start fee.** Misses, bad rows and notices are free.

***

### 🎯 Use cases: find emails from a spreadsheet

- **Outbound prospecting.** A conference or event list with names and company websites. Add the work email column.
- **CRM gap fill.** Contacts with a name and company but no email. Run the export and write the email back by row.
- **Agent workflows.** An AI agent holds a table of people and companies and needs addresses for all of them in one call.
- **Pair with a phone lookup.** Take the emails into [email-phone-finder](https://apify.com/b2bdata/email-phone-finder) for direct dials.

***

### 🚀 Quick start: find work emails for a list

#### 1. One row

```json
{
  "items": [{ "First Name": "Patrick", "Last Name": "Collison", "Website": "stripe.com" }],
  "columnMap": { "firstName": "First Name", "lastName": "Last Name", "domain": "Website" },
  "limit": 1
}
```

#### 2. A list with a full-name column and extra columns

```json
{
  "items": [
    { "Contact": "Patrick Collison", "Site": "https://www.stripe.com/", "Account": "Stripe", "id": 101 },
    { "Contact": "Satya Nadella", "Site": "microsoft.com", "Account": "Microsoft", "id": 102 }
  ],
  "columnMap": { "fullName": "Contact", "domain": "Site", "companyName": "Account" },
  "limit": 0
}
```

Columns you do not map (here `id`) are ignored.

#### 3. No columnMap

```json
{ "items": [{ "first_name": "Sam", "surname": "Altman", "Company Domain": "openai.com" }] }
```

***

### ⚙️ Inputs

| Key | Type | Default | What it does |
|---|---|---|---|
| `items` | array of objects | required | One object per CSV row, your own column names as keys. Rows must be objects; a row that is text or empty is listed in `MISSES` as `invalid_row`. |
| `columnMap` | object | auto-detect | Which of your columns holds what. Keys: `firstName`, `lastName`, `fullName`, `domain`, `companyName`, `linkedinUrl`. Values: your column header (case, spaces and punctuation do not matter). A mapped column that no row has stops the run before any charge. |
| `limit` | integer | 100 | Maximum rows looked up, which also caps the emails charged. `0` = all valid rows. Maximum 5,000. |
| `includeAudit` | boolean | false | Adds an `audit` object to each result. |

How a row is read: the name is first name + last name when both are filled, otherwise the full-name column. Both a first and a last name are required; a one-word name or a bare email address is not a name. The domain can be a plain domain or a website URL (`https://`, `www.` and paths are removed). The lookup uses the name and the domain. `companyName` and `linkedinUrl` are copied into `inputQuery` so you can trace a result to your own record.

Any other field stops the run before a lookup with `remove unsupported fields: ...`. Nothing is charged.

***

### 📤 Output

One result row per CSV row whose email was found on the domain in that row. Results keep input order. Empty values are left out.

| Key | Type | Meaning, and when it is missing |
|---|---|---|
| `rowIndex` | integer | Position of the row in your `items` (0 = first). Use it to join back |
| `fullName`, `firstName`, `lastName` | string | As matched, otherwise as you gave them |
| `email` | string | Work email on your domain, lower case |
| `emailStatus` | string | `valid`, `catch_all` or `unknown`. `unknown` is common: the address is not mailbox-checked. `invalid` is never delivered |
| `confidence` | number | 0-1, see the rule below |
| `matchMethod` | string | How the person was matched. Missing when not reported |
| `companyDomain` | string | The domain from your row, cleaned |
| `slug`, `profileUrl` | string | Professional profile slug and link, when matched |
| `headline`, `jobTitle` | string | Current, when on file |
| `location`, `country` | string | Free-text location and ISO-2 country, when on file |
| `connections` | integer | Connection count, when on file |
| `updatedAt` | string | When the person record was last updated, when on file |
| `inputQuery` | object | The cleaned input behind this result: `fullName`, `domain`, and your `companyName` and `linkedinUrl` when mapped |
| `scrapedAt` | string | ISO time the row was produced |
| `audit` | object | Only with `includeAudit` |

```json
{
  "rowIndex": 0,
  "fullName": "Patrick Collison",
  "firstName": "Patrick",
  "lastName": "Collison",
  "email": "patrick.collison@stripe.com",
  "emailStatus": "unknown",
  "confidence": 0.5,
  "matchMethod": "work-email",
  "companyDomain": "stripe.com",
  "slug": "patrickcollison",
  "headline": "Stripe CEO",
  "jobTitle": "CEO",
  "location": "San Francisco Bay Area",
  "country": "US",
  "connections": 500,
  "updatedAt": "2026-10-13T14:15:13.902Z",
  "inputQuery": { "fullName": "Patrick Collison", "domain": "stripe.com" },
  "scrapedAt": "2026-10-28T09:00:00.000Z"
}
```

*Which fields appear depends on what is on file for the person.*

#### Confidence rule

Base by status: `valid` 0.95, `catch_all` 0.6, `unknown` 0.5. Add 0.05 only when the response carries a profile link (`profileUrl`). Maximum 1. A `catch_all` domain accepts any address, so even a good status there is a weaker signal.

#### Other records in the run

- **`MISSES`** (key-value store): `[{rowIndex, input, reason}]` for every row without a result. Reasons: `not_found`, `no_address_on_domain`, `missing_name`, `missing_domain`, `invalid_name`, `invalid_domain`, `invalid_row`, `duplicate_row` (the same person and domain as an earlier row; `duplicateOf` names that row), or `Upstream HTTP <code>`.
- **`errors-bulk-email-finder`** (key-value store): the lookups that failed for a technical reason, with their `rowIndex`, so you can re-run only those rows.
- **`RUN_SUMMARY`**: counts (requested, valid, looked up, found, missed, rows not looked up because of `limit` or the free plan) and the stop reason.
- **Notice rows** (`type: "notice"`) appear when a free-plan list is longer than 100 (`free_plan_limit`), when the spend limit of your run is reached (`charge_limit_reached`), or when the input is invalid (`invalid-input`). Notices are never charged.

#### What is NOT returned

- Personal email addresses
- Phone numbers (use [email-phone-finder](https://apify.com/b2bdata/email-phone-finder))
- Several alternative addresses per person
- A live mailbox check of the address

***

### 🛡️ Reliable results

**Our data promise.** We work to keep every contact current, and we stand behind it. If a record is outdated, an email bounces or a person has moved on, open the Issues tab with your run link. We review every report and sort it out with you, case by case.

- **Misses are free and visible.** Every row without a result is in `MISSES` with its `rowIndex` and a reason and is never charged.
- **Only addresses on your domain.** An address on another domain is never returned as the result.
- **Duplicate rows are looked up once.** The same person and domain written twice is looked up and charged once; the repeat is in `MISSES` as `duplicate_row`.
- **Each result is saved as it is delivered.** A run that stops keeps what it already produced.
- **Bounded pace.** Lookups run at 60 per minute, so 1,000 rows need roughly 17 minutes and a full 5,000-row run about 83 minutes.
- **Typos fail early.** An unsupported input field, a wrong column name in `columnMap` or a limit above 5,000 stops the run before any charge.
- **Check before you send.** `emailStatus` and `confidence` are signals, not a guarantee. Run the emails through your own deliverability check before a campaign.

***

### 🤖 For AI agents (MCP)

This actor runs through the Apify MCP server. Prompt to copy:

> "Use b2bdata/bulk-email-finder with items=\[{"First Name":"Patrick","Last Name":"Collison","Website":"stripe.com"}], columnMap={"firstName":"First Name","lastName":"Last Name","domain":"Website"} and limit=1. Return rowIndex, email, emailStatus and confidence."

**When to use it.** You hold a table of people with first and last names and company domains and need one work email each.
**When not to.** You hold one person (use b2bdata/email-finder), only a company (use ceo-founder-email-finder-by-domain), or only an email (use email-phone-finder).
**Input contract.** `items` array of row objects; `columnMap` object (optional); `limit` 0-5,000 (default 100, 0 = all).
**Output contract.** One result per found row with `rowIndex`, `email`, `emailStatus`, `confidence`. Output is deterministic and schema-stable, results keep input order. Rows without a result are absent from the dataset and listed in `MISSES`.
**Smoke test.** `limit: 1` on a well-known person returns one result.
**Error cases.** `No valid rows` means no row had both a name and a domain. `remove unsupported fields` or a `columnMap` problem ends the run SUCCEEDED with one free `invalid-input` notice row and nothing charged.

***

### 🔌 Integrations: send results anywhere

1. **API.** Fetch results from the run's default dataset: `GET /v2/datasets/{datasetId}/items?format=csv`.
2. **Webhook** on run success to push results to your own endpoint.
3. **n8n, Make, Zapier.** Use the native Apify nodes: run the actor, then read the dataset.
4. **Google Sheets, Airtable, HubSpot, Salesforce, Clay.** Join on `rowIndex`, map `email` to the email field and `emailStatus` to a status column.

***

### 🕐 Schedule examples

- **Weekly CRM gap fill.** A Monday schedule with a saved task that points at your latest export.
- **New-lead enrichment.** A webhook from your form tool starts a run with the new rows.

***

### 🔗 Related actors

- `b2bdata/email-finder`: one person at a time, same lookup. [Open](https://apify.com/b2bdata/email-finder)
- `b2bdata/ceo-founder-email-finder-by-domain`: you know the company, not the person. [Open](https://apify.com/b2bdata/ceo-founder-email-finder-by-domain)
- `b2bdata/executive-contact-finder-by-domain`: get the decision makers at a domain first. [Open](https://apify.com/b2bdata/executive-contact-finder-by-domain)
- `b2bdata/email-phone-finder`: you have the email and need a phone. [Open](https://apify.com/b2bdata/email-phone-finder)

[See all actors by b2bdata](https://apify.com/b2bdata)

***

### ❓ FAQ

#### Data and coverage

**How do I find emails for a CSV list?**
Put your rows in `items` (one object per row, your column names as keys), map the columns in `columnMap` and run. Each found person becomes one result with the email, its status and a confidence score.

**Can I use my own column names?**
Yes. `columnMap` maps `firstName`, `lastName`, `fullName`, `domain`, `companyName` and `linkedinUrl` to your headers. Leave a key out and common header names are detected.

**How do I match the results back to my spreadsheet?**
Every result has `rowIndex`, the position of the row in your `items` (0 = first). Rows without a result are in `MISSES` with the same index.

**Do I need a LinkedIn account or cookies?**
No. It is a work email finder without login, cookies or an account to ban.

**Is there a confidence score per row?**
Yes. The score follows the fixed rule in the Output section: status first, plus a small bonus when the person was matched to a profile. It is a signal for sorting, not a promise.

#### Pricing

**What do I pay when nobody is found?**
Nothing. There is no start fee, and misses, unreadable rows, duplicates and notices are free.

***

### 🛟 Support

Use the **Issues** tab on this actor's Apify page for bugs, questions and feature requests. We answer within 48 hours on working days. Please include the run ID and the input you used. Published on Apify by b2bdata. Not affiliated with, endorsed by, or sponsored by any company or person named in the examples above.

# Changelog

This Actor's version history is a separate document: https://apify.com/b2bdata/bulk-email-finder/changelog.md

# Actor input Schema

## `items` (type: `array`):

Your list as an array of objects, one object per CSV row, with your own column names as keys. Example: \[{"First Name":"Patrick","Last Name":"Collison","Website":"stripe.com"}]. Each row needs a first and last name (or a full name) and a company domain. Results keep a rowIndex (0 = first row) so you can join them back to your CSV. Rows that cannot be looked up are listed in the MISSES record with their rowIndex and a reason, and are never charged.

## `columnMap` (type: `object`):

Maps the data to your column names. Keys: firstName, lastName, fullName, domain, companyName, linkedinUrl. Values: the column header in your rows. Example: {"firstName":"First Name","lastName":"Last Name","domain":"Website"}. Names and domain are used for the lookup; companyName and linkedinUrl are copied into inputQuery so you can trace a row. A key you leave out is auto-detected from common headers (first name, last, surname, name, domain, website, company, linkedin). A mapped column that is missing from the rows stops the run before any charge.

## `limit` (type: `integer`):

Maximum rows looked up in this run, which also caps the emails charged. 0 = all valid rows, up to the hard maximum of 5000. Above 5000 the run stops with a message; split the list.

## `includeAudit` (type: `boolean`):

Adds requestId, candidates, unitsCharged, latencyMs, inputRaw and inputCanonical to each row under audit. Omit for clean rows.

## Actor input object example

```json
{
  "items": [
    {
      "First Name": "Patrick",
      "Last Name": "Collison",
      "Website": "stripe.com"
    }
  ],
  "columnMap": {
    "firstName": "First Name",
    "lastName": "Last Name",
    "domain": "Website"
  },
  "limit": 100,
  "includeAudit": false
}
```

# Actor output Schema

## `emails` (type: `string`):

Row index, name, work email, validity status, confidence and profile details per person

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

Counts and stop reason for this run

# 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 = {
    "items": [
        {
            "First Name": "Patrick",
            "Last Name": "Collison",
            "Website": "stripe.com"
        }
    ],
    "columnMap": {
        "firstName": "First Name",
        "lastName": "Last Name",
        "domain": "Website"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("b2bdata/bulk-email-finder").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 = {
    "items": [{
            "First Name": "Patrick",
            "Last Name": "Collison",
            "Website": "stripe.com",
        }],
    "columnMap": {
        "firstName": "First Name",
        "lastName": "Last Name",
        "domain": "Website",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("b2bdata/bulk-email-finder").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 '{
  "items": [
    {
      "First Name": "Patrick",
      "Last Name": "Collison",
      "Website": "stripe.com"
    }
  ],
  "columnMap": {
    "firstName": "First Name",
    "lastName": "Last Name",
    "domain": "Website"
  }
}' |
apify call b2bdata/bulk-email-finder --silent --output-dataset

```

## MCP server setup

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

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/rBc5cRYPOXwPXhGZW/builds/vzpIfYPWu8JuZ5g4x/openapi.json
