# Work Email Finder: Find Email by Name and Domain (`rod_analytics/work-email-finder`) Actor

Find a person's work email from a name and a company domain. The Actor learns the company's email format from its own website, builds up to 3 candidates, checks MX and verifies them with MillionVerifier. Unverified guesses are labelled. No email found, no charge. From $0.003 per email.

- **URL**: https://apify.com/rod_analytics/work-email-finder.md
- **Developed by:** [Rod Services](https://apify.com/rod_analytics) (community)
- **Categories:** Lead generation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

**Work email finder by name and company domain**: find a person's work email from a first name, last name and a company domain. The Actor learns the company's email format from its own website, builds up to 3 candidates, checks MX and verifies them with MillionVerifier.
**From $0.003 per email found. Nothing found, nothing charged.** No account with an email data vendor needed.

### What you get

- **A work email per person**, with a status: `deliverable`, `published`, `catch-all-guess`, `unverified-guess` or `not-found`.
- **A confidence from 0 to 100**, the format the company uses (`{first}.{last}`, `{f}{last}`...) and how many published addresses support it.
- **Sources.** The pages of the company website that show the address or the format. Every row says `verifiedBy`, so a guess is never presented as a fact.
- **A published address when there is one.** If the company itself shows the person's address on its site, you get that one, no guessing.
- **Three candidates at most**, checked in order. The first deliverable one wins. Catch-all domains are labelled, not sold as verified.
- **Free rows for what could not be found**, with `errorCode`, `message` and `howToFix`.
- **Dataset input** for CRM exports and scraper results.

### How it works

1. The Actor opens the company website once per domain, reads `robots.txt` and the sitemap, and reads the team, press, contact, impressum and about pages. It learns the email format from the addresses the company itself publishes, using the person names printed next to them. `robots.txt` is always respected.
2. If the website shows this person's address, that address is returned (`published`).
3. Otherwise it builds the candidate from the format: `Anna Meier` at a `{first}.{last}` company is `anna.meier@company.com`. German umlauts get both spellings (`mueller`, `muller`). Without a pattern, a verifier lets it try the most common formats.
4. DNS gives the MX hosts. A domain with no mail server is `not-found` at once.
5. With a MillionVerifier key, the candidates are checked one by one. A mailbox that exists is `deliverable`. A domain that accepts everything is `catch-all-guess`. Each address is checked once per run.
6. Without a key you get the best candidate as `unverified-guess` with the label `unverified-pattern-guess`.

Sources are limited to **the company's own website**. No GitHub commit mining, no LinkedIn or other social network, no login, no CAPTCHA or bot bypass.

### Verification: what works today

| Mode | What you get |
| --- | --- |
| **Your own MillionVerifier key** (`millionVerifierApiKey`) | Up to 3 candidates per person are checked on your MillionVerifier account. Results: `deliverable`, `catch-all-guess`, `unverified-guess` if the server gives no clear answer. You pay MillionVerifier directly, and this Actor charges the plain found price. |
| **No key** | Published addresses from the company site, or the best pattern guess labelled `unverified-pattern-guess`. A guess is only returned when the website shows a clear format (confidence 50 or more). |
| **Hosted verification** | Built in, **not switched on yet**. When it is, verified emails are billed at the prices in the table below and you need no key. The status message of each run says which mode is active, and `verifiedBy` on every row tells how the address was checked. |

The public test keys `API_KEY_FOR_OK`, `API_KEY_FOR_CATCH_ALL`, `API_KEY_FOR_INVALID`, `API_KEY_FOR_UNKOWN` and `API_KEY_FOR_DISPOSABLE` from MillionVerifier work in `millionVerifierApiKey`. They return fixed answers and are never charged. Use them to see every result type.

### Quick start

1. Open the **Input** tab.
2. Add people to **People**, one object per person: first name, last name and company domain. The prefilled rows use placeholder names. Replace them with real people.
3. Optional: paste your MillionVerifier key to get verified results.
4. Press **Start**. The prefilled example with 3 people finishes in about a minute.
5. Open the **Output** tab. **Overview** shows the email per person, **Sources** the pages behind it, **Not found** why a row has no address.

#### From a contact scraper run to emails in 2 steps

1. Run [Website Contact Scraper: Emails, Phones & Social Links](https://apify.com/rod_analytics/company-contact-extractor) with **Include named people** on. It lists the named people the companies publish on their own sites.
2. Copy the Dataset ID of that run into **Dataset with people** here and press **Start**. Every named person who has no personal work email yet becomes one row. People who already have one are skipped.

### Input example

```json
{
    "people": [
        { "firstName": "Jane", "lastName": "Doe", "domain": "acme.com" },
        { "fullName": "Anna Meier", "companyWebsite": "https://www.example.org" },
        "Peter Müller, firma.de"
    ],
    "millionVerifierApiKey": "your key, optional"
}
```

| Field | Default | Description |
| --- | --- | --- |
| `people` | | Objects with `firstName` and `lastName` (or `fullName`) plus `domain` (or `companyWebsite`). Lines like `"Jane Doe, acme.com"` also work. Duplicates are removed. |
| `datasetId` | | Dataset of another run or a CRM export with names and company websites. |
| `firstNameField`, `lastNameField`, `fullNameField`, `domainField` | auto | Fields to read in that dataset. Dot paths work. |
| `millionVerifierApiKey` | | Optional. Your own MillionVerifier key for verified results. |
| `hunterApiKey` | | Optional. Your own Hunter.io key adds Hunter's format of the domain as one more vote. |
| `suppressionList` | | Emails, domains or SHA-256 hashes. Suppressed domains are skipped, suppressed addresses never returned. |
| `maxCandidates` | `3` | Candidates checked per person, 1 to 3. |
| `maxPagesPerDomain` | `8` | Pages read per domain, shared by all people at that domain. |
| `maxConcurrency` | `10` | People looked up in parallel. |

### Output sample

```json
{
    "inputRow": 1,
    "firstName": "Anna",
    "lastName": "Meier",
    "domain": "acme.com",
    "email": "anna.meier@acme.com",
    "status": "deliverable",
    "confidence": 97,
    "pattern": "{first}.{last}",
    "patternConfidence": 87,
    "supportingExamples": 7,
    "evidence": "site-pattern",
    "verifiedBy": "millionverifier-byok",
    "verification": "ok",
    "label": null,
    "catchAll": false,
    "sources": [{ "type": "pattern-evidence", "url": "https://acme.com/team" }],
    "candidatesTried": 1,
    "mx": { "found": true, "hosts": ["aspmx.l.google.com"], "provider": "Google Workspace", "note": null },
    "note": null,
    "error": false,
    "errorCode": null,
    "scrapedAt": "2026-10-07T09:30:00.000Z"
}
```

A row with no address is free:

```json
{
    "inputRow": 2,
    "firstName": "Jane",
    "lastName": "Doe",
    "domain": "small-shop.com",
    "email": null,
    "status": "not-found",
    "error": true,
    "errorCode": "no-pattern",
    "message": "The company website shows no clear email format and no verifier is available to try common ones.",
    "howToFix": "The site shows too few named email addresses to learn its format. Add your own MillionVerifier key to try the most common formats, or raise \"Pages per domain\"."
}
```

### Statuses

| Status | Meaning | Billed |
| --- | --- | --- |
| `deliverable` | The mail server confirmed the mailbox. | Yes |
| `published` | The company website shows this person's address. Not verified, or the server gave no clear answer. | Yes |
| `catch-all-guess` | The domain accepts every address, so nothing can be confirmed. The address follows the format the website shows. | Yes |
| `unverified-guess` | Built from the website's format. Nobody checked it. Label: `unverified-pattern-guess`. | Yes |
| `not-found` | No address. Reasons: `no-pattern`, `no-valid-candidate`, `no-mx`, `catch-all-no-pattern`, `disposable-domain`, `published-address-rejected`, `robots-blocked`, `bot-protection`, `site-unreachable`, `http-error`, `suppressed`, `missing-name`, `unsupported-name`, `missing-domain`, `invalid-domain`, `webmail-domain`. | Free |

For an unverified guess the confidence equals the confidence in the domain's format, capped at 85.

`evidence` says what the address rests on: `published-on-site` (the company shows it), `site-pattern` (built from the format the website shows), `hunter-pattern` (built from the format your own Hunter key reports, with no address on the website behind it) or `common-format` (a frequent format tried with a verifier, no evidence).

### Pricing

Pay per event. You pay for emails found, not for compute time or pages. Discounts apply automatically on paid Apify plans.

| Event | Free plan | Bronze | Silver | Gold |
| --- | --- | --- | --- | --- |
| Email found (published, pattern guess, or checked with your own key), per 1,000 | $3.00 | $2.70 | $2.40 | $2.10 |
| Verified deliverable email with hosted verification, per 1,000 | $25.00 | $22.50 | $20.00 | $17.50 |
| Catch-all guess with hosted verification, per 1,000 | $5.00 | $4.50 | $4.00 | $3.50 |
| Run start, per run | $0.001 | $0.001 | $0.001 | $0.001 |

- **Not found is free.** So are failed lookups, invalid rows and results from the public MillionVerifier test keys.
- The two hosted prices apply only when hosted verification is active. Today a run bills the plain "email found" price.
- With your own MillionVerifier key you pay MillionVerifier for each check on your account. This Actor does not add a verification fee.
- Set **Maximum cost per run** in the run options to cap spend. The Actor stops when the limit is reached.

### Use it from code, Make, Zapier or AI agents

Run it through the Apify API, the JavaScript and Python clients, schedules, webhooks, or integrations such as Make, Zapier, n8n, HubSpot and Google Sheets. AI agents can call it through the Apify MCP server at `mcp.apify.com`.

```bash
curl -X POST "https://api.apify.com/v2/acts/rod_analytics~work-email-finder/run-sync-get-dataset-items" \
  -H "Authorization: Bearer <YOUR_APIFY_TOKEN>" -H "Content-Type: application/json" \
  -d '{"people": [{"firstName": "Jane", "lastName": "Doe", "domain": "acme.com"}]}'
```

To see only the email format of a company, use [Email Format Finder: Company Email Pattern Lookup](https://apify.com/rod_analytics/email-format-finder). It uses the same pattern engine.

### GDPR, ePrivacy and responsible use

This Actor is built for **B2B use**. Read this before you run it on people in the EU or the UK.

- **You are the controller.** You choose the people and the purpose, so you are the data controller. This Actor acts as your processor and keeps nothing: results go only to your own dataset, nothing is stored on our side, and the log holds counts, never names or addresses.
- **You need a lawful basis**, for example a documented legitimate interest for B2B contact, and you must give each person a **privacy notice** (Art. 14 GDPR) at first contact or within one month, name the source, and honour objections and deletion requests. Keep objections in your suppression list.
- **Company addresses only.** Candidates are built only on the company's own domain. Free mail providers (gmail.com, outlook.com, gmx.de and others) are rejected, so private mailboxes are never guessed. A person's private address is never returned.
- **No sensitive inference.** Names are only compared with the local part of an address. Nothing about gender, origin, health or other traits is derived.
- **Sub-processor.** When verification is on, the candidate addresses are sent to MillionVerifier. Name it in your records of processing.
- **Suppression list.** `suppressionList` takes emails, domains or SHA-256 hashes. People at a suppressed domain are skipped and suppressed addresses are never returned.
- **Country notes.** **Germany: advertising emails need prior consent, also B2B (UWG section 7).** France: B2B outreach is allowed on legitimate interest when it relates to the person's job, with an easy way to object. Lithuania: no prior consent for legal-person subscribers, but an easy opt-out is required. Rules differ elsewhere, so check before you send.
- **Mind what a guess is.** A pattern guess is not proof that a mailbox exists. Do not send anything sensitive to an unverified guess.
- **Polite crawling.** `robots.txt` is respected, at most 2 parallel requests per site, no bot bypass.

This is not legal advice. Check your use case with your data protection officer.

### FAQ

**How accurate is it?** A `deliverable` result means the mail server said the mailbox exists. A `published` address is shown by the company itself. An `unverified-guess` is only as good as the format evidence behind it. Check `confidence`, `patternConfidence` and `supportingExamples`.

**Why "catch-all"?** Many company domains accept mail for any address, so no check can say whether a mailbox is real. Those rows are labelled `catch-all-guess`, billed at the lower price when hosted, and never shown as verified.

**What if the company website blocks the Actor?** Rows for those domains are free. With a MillionVerifier key the Actor still tries the most common formats. It does not bypass bot protection.

**Can I feed it a Google Maps or CRM export?** Yes, set `datasetId`. The Actor reads first name, last name or full name, and the company website or domain from each row.

**Why does the prefill use placeholder names?** The prefilled rows show how the output looks. Replace them with real people before you rely on the results.

**What happens if my input is wrong?** The run ends **SUCCEEDED** with one free help row: `error`, `errorCode`, `message` and `howToFix`. Only the start fee is charged. Codes: `no-valid-people`, `no-people-in-dataset`, `dataset-unreadable`, `bad-verifier-key`, `verifier-no-credits` and `bad-hunter-key`.

**Is my key safe?** MillionVerifier and Hunter keys are secret inputs. They are only sent to the vendor from your run and are never logged or stored by this Actor.

**Found a bug or need a custom field?** Open an issue on the **Issues** tab. Replies usually come within a day.

### Related Actors

| Actor | What it does |
| --- | --- |
| [Email Format Finder: Company Email Pattern Lookup](https://apify.com/rod_analytics/email-format-finder) | Find the email format of a company domain |
| [Website Content Crawler to Markdown for RAG & LLMs](https://apify.com/rod_analytics/website-to-markdown) | Crawl a site and get clean Markdown for RAG and LLMs |
| [PDF to Markdown & Text Extractor with OCR](https://apify.com/rod_analytics/doc-to-markdown) | Turn PDF, DOCX and scans into Markdown or text |
| [SEO Audit Tool: Site Crawler, Broken Links & Score](https://apify.com/rod_analytics/seo-site-audit) | Crawl a site for broken links and SEO issues |
| [PageSpeed Insights & Lighthouse Bulk Checker](https://apify.com/rod_analytics/lighthouse-audit) | Core Web Vitals and Lighthouse scores for many URLs |
| [Tech Stack Detector: Wappalyzer & BuiltWith Alternative](https://apify.com/rod_analytics/tech-stack-detector) | See which CMS, analytics and frameworks a site uses |
| [Bulk Domain Lookup: DNS, WHOIS, SPF, DMARC & SSL](https://apify.com/rod_analytics/domain-security-audit) | DNS, mail security and SSL checks for domain lists |
| [Speech to Text: Audio, Video & Podcast Transcriber](https://apify.com/rod_analytics/media-transcriber) | Transcribe audio, video and podcasts |
| [Email Validator & Verifier: Bulk MX & Disposable Check](https://apify.com/rod_analytics/bulk-email-verifier) | Clean email lists before you send |
| [Website Contact Scraper: Emails, Phones & Social Links](https://apify.com/rod_analytics/company-contact-extractor) | Company emails, phones and social links from websites |
| [VIES VAT Checker & EU Company Registry Lookup](https://apify.com/rod_analytics/eu-company-lookup) | Validate EU VAT numbers and look up companies |
| [Companies House Scraper: UK Company Search, No API Key](https://apify.com/rod_analytics/uk-companies-house) | UK company profiles, officers and filings |
| [EU TED Tenders Scraper & Alerts: Public Procurement](https://apify.com/rod_analytics/ted-tenders) | EU public tenders with daily alerts |
| [Greenhouse, Lever & Ashby Job Scraper: 8 ATS in One Feed](https://apify.com/rod_analytics/multi-ats-jobs-api) | Jobs from 8 ATS platforms in one feed |
| [GitHub Scraper: Repo Stats, Stars, Trending & Search](https://apify.com/rod_analytics/github-repo-stats) | Stars, activity and trending GitHub repos |
| [Hacker News Scraper](https://apify.com/rod_analytics/hackernews-scraper) | Stories, comments and Who is hiring threads from Hacker News |

# Changelog

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

# Actor input Schema

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

One object per person: firstName and lastName (or fullName) plus domain (or companyWebsite). Lines such as "Jane Doe, acme.com" also work. Duplicates are removed. The prefilled rows use placeholder names.

## `datasetId` (type: `string`):

Optional. Pick the dataset of a finished run or a CRM export with names and company websites. A Website Contact Scraper run with the option to include named people works directly: its people lists are expanded, and people who already have a work email are skipped.

## `firstNameField` (type: `string`):

Field with the first name. Leave empty to try firstName, first_name and givenName. Dot paths work.

## `lastNameField` (type: `string`):

Field with the last name. Leave empty to try lastName, last_name, familyName and surname.

## `fullNameField` (type: `string`):

Field with the full name when first and last name are not separate. Leave empty to try fullName and name.

## `domainField` (type: `string`):

Field with the company domain or website. Leave empty to try domain, companyDomain, companyWebsite, website and url. Google Maps and social links are skipped.

## `millionVerifierApiKey` (type: `string`):

Optional. With your own key, the Actor checks up to 3 candidates per person with MillionVerifier on your account, and returns a verified result. Without a key you get the best pattern guess, labelled unverified-pattern-guess. The public test keys API_KEY_FOR_OK, API_KEY_FOR_CATCH_ALL, API_KEY_FOR_INVALID, API_KEY_FOR_UNKOWN and API_KEY_FOR_DISPOSABLE work for tests and are never charged.

## `hunterApiKey` (type: `string`):

Optional. Adds Hunter's email format of the domain as one more vote. Uses at most one credit of your Hunter plan per domain. The key is only used in your run and never logged or stored by this Actor.

## `suppressionList` (type: `array`):

Email addresses, domains or SHA-256 hashes of them (of the lower-case address or domain). People at a suppressed domain are skipped, and a suppressed address is never returned.

## `maxCandidates` (type: `integer`):

How many likely addresses are checked per person, 1 to 3. Only used with a verifier key. The first deliverable one wins.

## `maxPagesPerDomain` (type: `integer`):

Homepage plus the best matching team, contact, press and impressum pages, read once per domain and shared by all people at it. The price does not depend on this number.

## `maxConcurrency` (type: `integer`):

How many people are looked up at the same time.

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

Maximum wait for one page. A whole domain is capped at 6x this value (at least 60 s).

## Actor input object example

```json
{
  "people": [
    {
      "firstName": "Jane",
      "lastName": "Doe",
      "domain": "sorainen.com"
    },
    {
      "firstName": "John",
      "lastName": "Smith",
      "domain": "tlu.ee"
    },
    {
      "fullName": "Anna Meier",
      "companyWebsite": "https://www.ut.ee"
    }
  ],
  "maxCandidates": 3,
  "maxPagesPerDomain": 8,
  "maxConcurrency": 10,
  "timeoutSecs": 15
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `sources` (type: `string`):

No description

## `notFound` (type: `string`):

No description

## `results` (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 = {
    "people": [
        {
            "firstName": "Jane",
            "lastName": "Doe",
            "domain": "sorainen.com"
        },
        {
            "firstName": "John",
            "lastName": "Smith",
            "domain": "tlu.ee"
        },
        {
            "fullName": "Anna Meier",
            "companyWebsite": "https://www.ut.ee"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("rod_analytics/work-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 = { "people": [
        {
            "firstName": "Jane",
            "lastName": "Doe",
            "domain": "sorainen.com",
        },
        {
            "firstName": "John",
            "lastName": "Smith",
            "domain": "tlu.ee",
        },
        {
            "fullName": "Anna Meier",
            "companyWebsite": "https://www.ut.ee",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("rod_analytics/work-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 '{
  "people": [
    {
      "firstName": "Jane",
      "lastName": "Doe",
      "domain": "sorainen.com"
    },
    {
      "firstName": "John",
      "lastName": "Smith",
      "domain": "tlu.ee"
    },
    {
      "fullName": "Anna Meier",
      "companyWebsite": "https://www.ut.ee"
    }
  ]
}' |
apify call rod_analytics/work-email-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rod_analytics/work-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/HjhB0LlIOVNme09oL/builds/4AgVkyMRvImnxVm1Z/openapi.json
