# Email Pattern Finder - Work Email Format by Name + Domain (`flash_scraper/email-pattern-finder`) Actor

Work email finder: give a person's name and a company domain and get ranked address guesses (first.last@, flast@, first@...). Crawls the company site to detect its real email format and pattern, and MX-checks the domain + provider. Confidence-scored, no API key, pay-per-result.

- **URL**: https://apify.com/flash\_scraper/email-pattern-finder.md
- **Developed by:** [Flash Scrape](https://apify.com/flash_scraper) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 3 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 person processeds

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?

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

## Work Email Finder - Email Format & Pattern by Name + Domain

**Email pattern finder for any company: find the email address pattern a company uses** and turn plain names into ranked work-email guesses. Give it a company domain and a list of full names; it reads a few public pages of the company's own site to observe its real address format, returns each person's most likely addresses (`first.last@`, `flast@`, `first@`…) scored by evidence and pattern frequency, and runs a DNS check that the domain can actually receive mail and which provider runs it (Google Workspace vs Microsoft 365). It's the natural front end of a **find → verify → enrich** prospecting flow — **no API key, no proxies, no per-seat email-finder subscription**, just pay-per-result.

### What it does

- **Detects the company's actual email pattern from its website** (`crawlForPattern`, on by default): reads up to 9 public pages (home, about, team, people, staff, contact) for real same-domain addresses. If one matches an input name, or has a personal shape (`jane.diaz@`), that pattern is promoted to **98 confidence** and flagged `observed` for every name. Role aliases (`info@`, `sales@`, `customer.service@`) never count as evidence.
- **Generates up to 12 ranked email candidates per person** from a library of common corporate patterns: `first.last`, `flast`, `firstlast`, `f.last`, `first`, `first_last`, `firstl`, `first-last`, `lastfirst`, `last.first`, `lfirst`, `last`.
- **Scores each candidate 0–99** — 98 means observed on the company's own site; below that, the score is the pattern's real-world frequency prior (`first.last` = 95 … `last` = 35) — and surfaces the single `best_guess` per person.
- **Checks the domain's MX records** via DNS-over-HTTPS to confirm the domain can receive mail at all — if no mail server exists, every candidate's confidence is cut to 60% of its base, so dead domains can't inflate your list. If the check itself cannot complete, the row says so (`mx_status: lookup_failed`, `mx_found: null`) instead of pretending the domain is dead.
- **Detects the email provider** from the MX host (Google Workspace, Microsoft 365, Proofpoint, Zoho, or other) — useful both as a deliverability signal and as a firmographic data point.
- **Normalizes names**: strips accents (`José` → `jose`), drops titles and credentials (`Dr. Jane Doe, PhD` → `jane.doe@`) and parenthesised notes, skips duplicates, and handles single-name inputs (those only get the `first@` pattern rather than fabricated surnames).
- **Accepts messy domain input** — `acme.com`, `https://www.acme.com/about`, `acme.com:443`, even `jane@acme.com` — it all resolves to the bare domain.
- Lets you **restrict generation to specific pattern IDs** when you already know the company's format and just want it applied to a batch of names.

Everything is **read-only and privacy-respecting**: the actor builds address strings from the names you provide, performs an MX lookup on the domain, and fetches at most nine public pages of the company's website (no proxies, no login). It never sends an email and never probes individual mailboxes.

### Use cases

- **Sales prospecting** — you found 20 decision-makers on LinkedIn at a target account but no contact info. Paste the names + domain, get ranked address guesses in seconds instead of paying per-credit on Hunter or Apollo.
- **Recruiting outreach** — reach engineers and managers directly at their work address instead of fighting the InMail queue.
- **Lead-list completion for agencies** — you're delivering a lead list on Fiverr/Upwork and half the rows have a name and company but no email; batch-fill the gap and mark each row with its confidence score.
- **CRM enrichment** — export contacts missing an email, group them by company domain, run each group through the finder, and re-import the `best_guess` column.
- **Pattern discovery** — confirm one address for any employee (from a signature, a press release), see which pattern it matches, then re-run the actor with that pattern locked in via `patterns` for the whole department.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `domain` | string | *(required)* | The company's email domain, e.g. `acme.com`. A URL, port or pasted address is reduced to the bare domain. One domain per run. |
| `names` | array | *(required)* | People to generate addresses for, one full name per line (e.g. `Jane Doe`). Titles/credentials stripped, duplicates skipped. Each usable name is one billed row. |
| `verifyMx` | boolean | `true` | Look up the domain's MX records (DNS-over-HTTPS) to confirm it can receive mail and detect the provider. No mailbox probing, no email sent. |
| `crawlForPattern` | boolean | `true` | Read up to 9 public pages of the company site for real same-domain addresses and infer the company's actual pattern from them. |
| `maxCandidatesPerPerson` | integer | `6` | How many ranked addresses to return per person (1–12). The observed pattern, when there is one, is always in the list. |
| `patterns` | array | *(empty)* | Optional: only generate these pattern ids (see the list above). Unknown ids are ignored and named in the status message. Leave empty for all 12. |

Example input:

```json
{
  "domain": "stripe.com",
  "names": ["Patrick Collison", "Jane Doe"],
  "verifyMx": true,
  "crawlForPattern": true,
  "maxCandidatesPerPerson": 6
}
```

### Output

One dataset row per person:

| Field | Description |
|---|---|
| `name` / `first_name` / `last_name` | The input name and its parsed (lower-cased, accent-free) parts |
| `domain` | Normalized company domain |
| `mx_found` | `true` = mail servers found, `false` = the domain has no MX record, `null` = check skipped (`verifyMx: false`) or could not complete — see `mx_status` |
| `email_provider` | Detected provider from MX (e.g. Google Workspace, Microsoft 365) |
| `best_guess` | The single most likely address |
| `best_guess_confidence` | Its 0–99 score (98 = pattern observed on the company site) |
| `pattern_source` | `observed_name_match` (an input name's real address was found), `observed_structure` (a personal address of that shape was found), or `heuristic` |
| `observed_pattern` | The pattern id the site evidence points to, or `null` |
| `evidence_email` | The real address that proved the pattern, or `null` |
| `observed_emails` | Up to 10 same-domain addresses found on the site (raw, may include role aliases) |
| `candidates` | All ranked guesses, each with `email`, `pattern`, `confidence` (and `evidence: "observed"` on the promoted one) |
| `best_guess_pattern` | The pattern id of `best_guess` |
| `mx_status` | `found`, `none`, `lookup_failed` or `skipped` |
| `pages_crawled` | How many company pages were actually read (`0` = the site could not be read; `null` = crawl off) |

Example row:

```json
{
  "name": "Jane Doe",
  "first_name": "jane",
  "last_name": "doe",
  "domain": "stripe.com",
  "mx_found": true,
  "email_provider": "Google Workspace",
  "best_guess": "jane.doe@stripe.com",
  "best_guess_confidence": 98,
  "pattern_source": "observed_structure",
  "observed_pattern": "first.last",
  "evidence_email": "jane.diaz@stripe.com",
  "observed_emails": ["jane.diaz@stripe.com"],
  "candidates": [
    { "email": "jane.doe@stripe.com", "pattern": "first.last", "confidence": 98, "evidence": "observed" },
    { "email": "jdoe@stripe.com", "pattern": "flast", "confidence": 88 },
    { "email": "janedoe@stripe.com", "pattern": "firstlast", "confidence": 78 },
    { "email": "j.doe@stripe.com", "pattern": "f.last", "confidence": 72 },
    { "email": "jane@stripe.com", "pattern": "first", "confidence": 65 },
    { "email": "jane_doe@stripe.com", "pattern": "first_last", "confidence": 60 }
  ],
  "best_guess_pattern": "first.last",
  "mx_status": "found",
  "pages_crawled": 4
}
```

Export to **CSV, Excel, or JSON** from the Apify Console or via API.

#### Example output

A sample with the site crawl off (`crawlForPattern: false`), so every score is the heuristic prior:

| name | domain | best\_guess | best\_guess\_confidence | mx\_found |
|---|---|---|---|---|
| Patrick Collison | stripe.com | patrick.collison@stripe.com | 95 | true |
| John Smith | stripe.com | john.smith@stripe.com | 95 | true |
| Jane Doe | stripe.com | jane.doe@stripe.com | 95 | true |
| Maria Garcia | stripe.com | maria.garcia@stripe.com | 95 | true |

### How the confidence score works

- **98** — the pattern was **observed on the company's own website** (`pattern_source` tells you whether an input name's real address was found, or a personal address of that shape). Strongest signal the actor can give without probing mailboxes.
- **95 … 35** — the pattern's **frequency prior**: how common that format is across corporate mailboxes (`first.last` 95, `flast` 88, `firstlast` 78, `f.last` 72, `first` 65, `first_last` 60, `firstl` 52, `first-last` 48, `lastfirst` / `last.first` 44, `lfirst` 40, `last` 35).
- **× 0.6 (floor 5)** when the domain has **no MX record** (`mx_status: none`) — it cannot receive mail at that apex. This applies to the observed pattern too (98 → 58), which therefore still ranks first and keeps its `evidence: "observed"` flag.
- **Unchanged** when the MX check could not complete (`mx_status: lookup_failed`) — the actor never turns a DNS outage into "this domain is dead".

### Every run comes with a report

Each delivering run also saves a one-page HTML **REPORT** in the run's key-value store (Output tab → `REPORT`, or the link in the run's status message): the delivered count, the detected pattern and its evidence, the MX/provider result, the best-guess pattern split and confidence bands, the first 100 rows, and the run's notes. It is a single self-contained HTML file, safe to forward or screenshot. The dataset stays the source of truth.

### What the status message and RUN\_SUMMARY tell you

- A successful run's status message states **exactly how many person rows were delivered** (= billed), how the pattern was decided (`observed pattern first.last (evidence …)` or `heuristic ranking`), the mail provider, and every caveat: names skipped or duplicated, a domain with no MX record, an MX check that could not complete, a company site that could not be read (e.g. HTTP 403 from datacenter IPs), a charge-limit trim, or a run-time budget cut.
- A zero-row run exits **SUCCEEDED** (no stack trace) with the real cause — no domain, no names, unknown pattern ids, single-word names with two-part patterns only, or a charge limit that leaves no budget — and always ends with **"You were not charged."** A failed lookup is never reported as "no results".
- Every run also writes a `RUN_SUMMARY` key-value record (`status`, `delivered`, `charge_requested`/`charge_confirmed`, `billing`, `cause`, `domain`, name counts, `mx_status`, `email_provider`, `pattern_source`, `observed_pattern`, `evidence_email`, `pages_crawled`, `crawl_errors`, `deadline_hit`, `budget_trimmed`, `report_url`) for pipelines that want a machine-readable verdict. `status` is `delivered`, `zero_rows`, `push_failed` or — when the run crashed before delivering anything — `failed` with the exception in `cause` (the run itself then ends FAILED, and the status message says you were not charged). `billing` records how the rows were billed (`pay-per-event`, or `platform:<model>` when the platform bills per dataset item); `charge_confirmed` is `null` in the latter case rather than a misleading `0`.

### Reliability

- Every network wait (MX lookup, each page read) is bounded by the run's **own remaining time** with a reserve for delivery, so a short `timeoutSecs` still returns rows instead of a timed-out empty run.
- Page bodies are capped at 1 MB and scanned in linear time — one base64-heavy marketing page can no longer stall the run.
- Rows are pushed in bounded chunks **before** billing; exactly the pushed rows are charged, once. If a later chunk fails, only the rows that landed are billed.

### Pricing

**Pay-per-result**: you're charged only per person row delivered — a fraction of a cent each. Names that produced no address, duplicates and rows trimmed by your charge limit are never billed. No subscription, no monthly credit packs that expire. A **free Apify plan** is enough to run your first batches and test the output on real accounts before paying anything.

### Tips / FAQ

#### Does it verify that the mailbox actually exists?

No — and that's deliberate. The actor generates **pattern-based guesses** ranked by site evidence and real-world commonality and verifies the **domain** (MX lookup), not individual mailboxes. It performs no SMTP handshakes and sends nothing. For mailbox-level checking, pipe the `best_guess` column into the [Bulk Email Verifier](https://apify.com/flash_scraper/email-verifier).

#### How accurate are the guesses?

When the company site publishes even one personal address, the observed pattern (98) is right for most colleagues at companies with standardized IT. Without evidence, `first.last@` alone covers the largest share of corporate mailboxes, which is why it scores 95. Accuracy is lower at very large enterprises with legacy formats or duplicate-name collisions. The confidence score tells you how much to trust each row — and if you can confirm one real address at the company yourself, lock its pattern via `patterns` for the whole batch.

#### `pages_crawled` is 0 — what happened?

The company site could not be read from the Apify datacenter (typically HTTP 403 from a bot shield, or the site is down); the status message names the response. Ranking then falls back to the heuristic priors. `pages_crawled` > 0 with `pattern_source: heuristic` means the pages were read but published no personal address.

#### What happens with single names or names with accents or titles?

Accents and diacritics are transliterated (`René Müller` → `rene.muller@…`); `Dr.`, `Prof.`, `PhD`, `Jr.` and parenthesised notes are dropped. Tokens that are both a credential and a real name are kept as names unless written in their credential form: `Anh Do` → `anh.do@`, `Hon Lam` → `hon.lam@`, but `Jane Doe, DO` → `jane.doe@` and `Hon. Jane Doe` → `jane.doe@`. A single-token name (e.g. just `Madonna`) only generates the `first@` pattern — the actor won't invent a surname. Names without Latin letters cannot be built and are skipped without charge.

#### Why did all my confidences drop for one domain?

The MX check found no mail servers (`mx_status: none`), so every candidate's score was reduced to 60% of its base (floor 5) and `mx_found` is `false`. That usually means the domain can't receive mail at that apex — check whether the company actually uses a different email domain. If `mx_found` is `null` with `mx_status: lookup_failed`, the check itself could not complete and scores were left untouched; re-run to get it.

#### Can I process people from many different companies in one run?

One run handles **one domain** with any number of names. For a multi-company list, group your rows by domain and trigger one run per domain via the Apify API — each run is billed only for the people it delivers.

#### How do I integrate it?

Start runs from the [Apify API](https://docs.apify.com/api/v2) or SDK, schedule recurring runs with Apify **Schedules**, and export the dataset as CSV/Excel/JSON or push it onward with Apify integrations (webhooks, Zapier, Make). Read `RUN_SUMMARY` from the run's key-value store for a machine-readable verdict.

### Related actors

- [Bulk Email Verifier](https://apify.com/flash_scraper/email-verifier) — the natural next step: verify and score the guessed addresses before you send.
- [Company & Domain Enricher](https://apify.com/flash_scraper/company-domain-enricher) — enrich the same domains with company name, socials, and tech stack.
- [Phone Number Validator](https://apify.com/flash_scraper/phone-number-validator) — clean the phone column of the same lead list.

***

Found a bug or missing a feature? Open an issue on this actor's **Issues tab** — typical response within 1 business day.

### Related Flash Scrape actors

- [Multi Job Board Scraper](https://apify.com/flash_scraper/multi-jobboard-scraper) — LinkedIn, Indeed, Glassdoor + 9 more boards, deduplicated, with only-new job alerts
- [Local Business Leads Scraper](https://apify.com/flash_scraper/local-business-leads) — any category, any city, verified emails, lead scores
- [Remote Jobs Aggregator](https://apify.com/flash_scraper/remote-job-aggregator) — 10 keyless remote boards in one deduplicated feed
- [All Flash Scrape actors](https://apify.com/flash_scraper) — same house rules everywhere: pay per delivered row, honest status messages, only-new monitoring, webhook alerts, and a run report on every run.

***

### More from Flash Scrape

Same publisher, same rules: no API keys, pay per row, filters run before billing.

- [Multi Job Board Scraper](https://apify.com/flash_scraper/multi-jobboard-scraper) - LinkedIn, Indeed, Glassdoor and 9 more boards in one deduplicated table
- [Local Business Leads Scraper](https://apify.com/flash_scraper/local-business-leads) - local businesses in any category and city with MX-verified emails, phones and socials
- [Remote Jobs Aggregator](https://apify.com/flash_scraper/remote-job-aggregator) - 10 remote job boards in one feed, only-new-jobs monitoring
- [Creator Leads Scraper](https://apify.com/flash_scraper/creator-leads-scraper) - YouTube, TikTok and Instagram creator emails in one run

# Actor input Schema

## `domain` (type: `string`):

The company's email domain, e.g. 'acme.com'. A full URL ('https://www.acme.com/about'), a port or a pasted address ('jane@acme.com') is reduced to the bare domain. One domain per run.

## `names` (type: `array`):

People to generate addresses for, one full name per line (e.g. 'Jane Doe'). Titles and credentials ('Dr.', 'PhD', 'Jr.') and parenthesised notes are stripped; duplicates are skipped; a single-word name only gets the 'first' pattern. Each usable name is one billed row.

## `verifyMx` (type: `boolean`):

Look up the domain's MX records (DNS-over-HTTPS) to confirm it can receive mail and detect the provider (Google Workspace, Microsoft 365, ...). No MX record cuts every confidence to 60%; a lookup that cannot complete leaves mx\_found null and confidences unchanged (see mx\_status). No mailbox probing, no email sent.

## `crawlForPattern` (type: `boolean`):

Read up to 9 public pages of the company site (home, about, team, contact, ...) for real same-domain addresses and infer the company's ACTUAL pattern from them - evidence, not priors. An observed pattern is promoted to 98 confidence and flagged 'observed' for every name. Role aliases (info@, sales@) never count as evidence. pages\_crawled tells you whether the site could be read at all.

## `maxCandidatesPerPerson` (type: `integer`):

How many ranked addresses to return per person (1-12). The observed pattern, when there is one, is always in the list.

## `patterns` (type: `array`):

Optional: only generate these pattern ids. Valid ids: first.last, flast, firstlast, f.last, first, first\_last, firstl, first-last, lastfirst, last.first, lfirst, last. Unknown ids are ignored (and named in the status message); leave empty for all 12.

## Actor input object example

```json
{
  "domain": "stripe.com",
  "names": [
    "Patrick Collison",
    "Jane Doe"
  ],
  "verifyMx": true,
  "crawlForPattern": true,
  "maxCandidatesPerPerson": 6
}
```

# Actor output Schema

## `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 = {
    "domain": "stripe.com",
    "names": [
        "Patrick Collison",
        "Jane Doe"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("flash_scraper/email-pattern-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 = {
    "domain": "stripe.com",
    "names": [
        "Patrick Collison",
        "Jane Doe",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("flash_scraper/email-pattern-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 '{
  "domain": "stripe.com",
  "names": [
    "Patrick Collison",
    "Jane Doe"
  ]
}' |
apify call flash_scraper/email-pattern-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,flash_scraper/email-pattern-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/mvb626RINfBd2keil/builds/dp7koqwF0F5TGvdiO/openapi.json
