# Work Email Finder - Name + Domain to Email, Pattern & Score (`scrapersdelight/work-email-finder-scraper`) Actor

Give it a first name, last name and company domain; get back ranked work-email candidates. Each guess is built from REAL addresses observed at that domain (GitHub, npm, team pages) plus MX/SPF/DMARC, so the confidence score is earned, not invented. Source URLs on every row. No fake SMTP checks.

- **URL**: https://apify.com/scrapersdelight/work-email-finder-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Lead generation, Automation, Business
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 per person resolveds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/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 — Name + Domain → Email, Pattern & Score

**Give it a first name, a last name and a company domain. Get back a ranked ladder of work-email
candidates — where the pattern was decoded from REAL addresses the company already published, with
the source URLs on every row so you can check the answer yourself.**

Most email finders hand you `first.last@company.com` and a confidence number they will not explain.
On the 16 domains where this actor found decodable evidence in its benchmark run, **8 of them do not
use `first.last`** — Twilio, Cloudflare, HubSpot, Jones Day and Sidley use `{f}{last}`, McKinsey uses
`{first}_{last}`, Stripe leans `{first}{last}`. A ranker that defaults to `first.last` is wrong half
the time and never tells you.

This one shows its work:

| What you also get | Why it matters |
|---|---|
| `evidenceSamples[]` | The actual `Name → address` pairs we found, each with the URL it came from |
| `patternVotes` + `patternAgreement` | How the domain's addresses split across patterns — 100% or 43%? |
| `candidates[]` | The full ranked ladder, because on a split domain the answer is often #2 |
| `mailDomain` | mwe.com's people are reachable at **mcdermottlaw.com** — appending the input domain returns dead addresses |
| MX / SPF / DMARC / DKIM / BIMI / RDAP | The domain's whole public mail posture, on every row |
| `blockedSources[]` | When a source blocked us we say so — "blocked" is never reported as "nothing found" |

***

### 🔍 What it does

1. **DNS tier (always runs, direct, never proxied).** MX, SPF, DMARC, BIMI and 18 DKIM selectors over
   DNS-over-HTTPS, plus RDAP registration facts. This tier alone guarantees every person gets a row.
2. **Evidence tier (public sources only).** The company's GitHub org commits (author name + address),
   npm maintainers/contributors, PyPI metadata, and the company's own website — homepage, then
   `robots.txt` → sitemap-discovered team / people / contact pages, plus `/.well-known/security.txt`.
3. **Decode, don't guess.** Every observed `Name → address` pair is tested against 20 pattern
   templates. A pattern only earns a vote if rendering it with that name reproduces the local part
   **exactly**. `Michael Broshi → mbroshi` votes `{f}{last}`; `David Brownman → xavdid` votes for
   nothing, and is reported as undecodable rather than forced into a bucket.
4. **Rank and score.** Confidence is `agreement × a sample-size factor`, so a domain that splits 43%
   five ways cannot come back "high".

#### Sources verified live (2026-09-02/03, direct **and** through the Apify proxy)

| Source | Request | Result |
|---|---|---|
| DNS-over-HTTPS | `GET cloudflare-dns.com/dns-query?name=stripe.com&type=MX` (`accept: application/dns-json`) | HTTP 200, 517 bytes, `10 aspmx.l.google.com.` |
| DMARC | `GET .../dns-query?name=_dmarc.stripe.com&type=TXT` | HTTP 200, `v=DMARC1; p=reject; ... rua=mailto:dmarc-reports@stripe.com` |
| RDAP | `GET rdap.org/domain/stripe.com` (follows redirect) | HTTP 200, 2,834 bytes, registration `1995-09-12T04:00:00Z` |
| GitHub | `GET api.github.com/repos/stripe/stripe-node/commits?per_page=100` | HTTP 200, 486,440 bytes, 56/100 commits from `@stripe.com` |
| npm | `GET registry.npmjs.org/-/v1/search?text=stripe&size=20` | HTTP 200, 25,654 bytes, maintainer addresses at the domain |
| Website | `GET www.mwe.com/people/` | HTTP 200, 369,600 bytes, 11 addresses, names recoverable from embedded JSON |

***

### 📥 Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `people` | array (JSON) | — | The list to resolve: `[{ "firstName":"Michael", "lastName":"Broshi", "domain":"stripe.com" }]`. `fullName` works instead of first/last; `companyDomain` / `website` work instead of `domain`; a full URL is reduced to its host. |
| `personLines` | array (strings) | `[]` | Paste-friendly alternative: `Michael Broshi, stripe.com` or `Michael, Broshi, stripe.com`. |
| `firstName` / `lastName` / `companyDomain` | string | — | Convenience path for one person. |
| `useGithub` | boolean | `true` | Read the company's public GitHub org commits. The richest source on technology companies; contributes nothing on a domain with no org. |
| `useRegistries` | boolean | `true` | Read npm + PyPI package metadata. |
| `useWebsite` | boolean | `true` | Read the company site: homepage → sitemap-discovered team/people/contact pages → `security.txt`. This is the tier that works on non-technology companies. |
| `maxWebsitePages` | integer | `8` | Discovered pages to read per domain, on top of the homepage (0–30). |
| `maxGithubRepos` | integer | `3` | Most-recently-pushed public repos to read 100 commits from (0–10). |
| `githubToken` | string (secret) | — | Optional read-only GitHub PAT. Unauthenticated GitHub allows **60 requests/hour per IP**; a token raises it to 5,000. No scopes needed. |
| `maxCandidates` | integer | `8` | Length of the ranked `candidates[]` ladder (1–20). |
| `maxSecondsPerDomain` | integer | `75` | Hard evidence budget per domain. When it expires the row still ships from what was gathered plus DNS/RDAP, and `blockedSources` says so. |
| `maxPeople` | integer | `1000` | Safety cap on people per run (0 = no cap, 5,000 hard limit). |
| `proxyConfiguration` | object | `{ "useApifyProxy": true }` | Applies to GitHub / npm / PyPI / websites. **DNS always runs direct.** Switch to `RESIDENTIAL` if a company site refuses datacenter IPs. |

```json
{
  "people": [
    { "firstName": "Michael", "lastName": "Broshi", "domain": "stripe.com" },
    { "firstName": "Jeff",    "lastName": "Lawson", "domain": "twilio.com" }
  ],
  "maxWebsitePages": 8,
  "maxGithubRepos": 3
}
```

Evidence is **cached per domain**, so 50 people at one company cost barely more work than the first.

***

### 📤 Output — one row per person, 70 fields

#### Identity + answer

| Field | Type | Description |
|---|---|---|
| `firstName`, `lastName`, `fullName` | string | Echoed from the input, so the row joins back to your CRM. |
| `companyDomain` | string | The domain you supplied. |
| `inputIndex` | integer | Position in your input list. |
| `email` | string | The top-ranked candidate. |
| `emailPattern` | string | The pattern that produced it, e.g. `{f}{last}`. |
| `confidence` | number | 0–0.95. Driven by pattern agreement, damped by sample size. |
| `confidenceLabel` | string | `high` ≥0.75 · `medium` ≥0.50 · `low` ≥0.30 · `very-low` below. |
| `confidenceReason` | string | Plain English: what we proved and what we did not. |
| `matchType` | string | `observed-direct` (we found this person's actual published address) · `pattern-inferred` · `provider-heuristic` (no evidence — generic prior). |
| `candidates[]` | array | `{ email, pattern, score, votes }`, best first. |
| `candidateCount` | integer | Length of that ladder. |

#### The audit trail (this is the product)

| Field | Type | Description |
|---|---|---|
| `evidenceCount` | integer | Real addresses observed at this company's mail domain. |
| `evidenceDecodedCount` | integer | How many of those we could tie to a named person and decode into a pattern. |
| `patternVotes` | object | `pattern → vote count`. An ambiguous pair splits its vote. |
| `patternAgreement` | number | Top pattern's share of the vote. `1` = unanimous, `0.43` = a five-way split. |
| `evidenceSamples[]` | array | Up to 12 `{ name, email, pattern, source, url, nameTrust, nameOrderAmbiguous }` — the receipts. |
| `evidenceSources[]` | array | `github-commit`, `npm-package`, `npm-maintainer`, `pypi`, `website`. |
| `evidenceUrls[]` | array | Every URL we read for this domain. Open them and check us. |
| `patternSource` | string | `decoded-from-observed-addresses` · `structural-shape-of-observed-addresses` · `generic-prior`. |
| `blockedSources[]` | array | Sources that refused us (anti-bot interstitial, HTTP 403, GitHub rate limit, budget expiry). |
| `websitePagesRead` | integer | Company pages actually read. |

#### Where the mail actually lives

| Field | Type | Description |
|---|---|---|
| `mailDomain` | string | The domain the addresses are really on. |
| `mailDomainDiffersFromInput` | boolean | `true` when it is not the domain you supplied. |
| `mailDomainBasis` | string | Why we picked it. |
| `webDomain` | string | Host the company website resolved to after redirects. |
| `hasMx`, `mxRecords[]`, `mxHostCount` | bool / array / int | MX hosts with priorities. |
| `mailProvider`, `mailProviderBasis` | string | Google Workspace, Microsoft 365, Proofpoint, Mimecast, Zoho, Proton, Fastmail, Cloudflare Email Routing and ~20 more — plus the exact MX host or SPF include that identified it. |
| `spfRecord`, `spfIncludes[]`, `spfPolicy`, `hasSpf` | string / array | Full SPF record, its `include:` list, and the `all` qualifier (`-all`, `~all`, …). |
| `dmarcRecord`, `dmarcPolicy`, `dmarcReportAddress`, `hasDmarc` | string / bool | DMARC record, `p=` policy, and the `rua=` reporting mailbox. |
| `dkimSelectorsFound[]` | array | Which of 18 common DKIM selectors publish a key. |
| `bimiRecord`, `bimiLogoUrl` | string | BIMI record and logo, when present. |
| `catchAllLikely`, `catchAllBasis` | bool / string | See honest limits — for Google/Microsoft domains this says *no verifier can tell a real mailbox from a fake one here*. |

#### Company + domain facts

| Field | Type | Description |
|---|---|---|
| `domainCreatedDate`, `domainAgeDays`, `domainExpiryDate` | string / int | From RDAP. |
| `domainStatus[]`, `domainRegistrarId`, `domainRegistrar` | array / string | EPP statuses, IANA registrar ID and name. |
| `companyNameFromRegistry` | string | RDAP registrant organisation. **Redacted by ICANN privacy on effectively every gTLD — expect `null`.** |
| `companyWebsiteStatus`, `companyTitle` | int / string | HTTP status and `<title>` of the company homepage. |
| `robotsDisallowsPeoplePaths` | boolean | Whether the site's `robots.txt` disallows people/team-shaped paths. |
| `githubOrg`, `githubOrgUrl`, `githubPublicRepos` | string / int | The GitHub org we matched to this domain. |
| `securityTxtContact` | string | `Contact:` line from `/.well-known/security.txt`. |
| `isRoleAddress`, `isFreeMailDomain`, `isDisposableDomain` | boolean | Sanity flags on the chosen address and the mail domain. |
| `roleAddressesFound[]` | array | Role mailboxes seen at the domain (`info@`, `press@`, …). |

#### Honesty fields

| Field | Type | Description |
|---|---|---|
| `smtpVerified` | boolean | **Always `false`.** See honest limits. |
| `smtpVerificationAvailable` | boolean | **Always `false`.** |
| `verificationMethod` | string | Exactly what produced this answer. |
| `undeliverableReason` | string | Set when the domain cannot receive mail (no MX, free-mail, disposable). |
| `resolvedAt`, `sourcesQueried[]`, `lookupDurationMs` | string / array / int | Provenance and cost of this domain's lookup. |

***

### 📊 Measured — run `QOb1MsFAxLfhsskMe`, build `0.1.4`, 2026-09-03

Not estimates. This is one real Apify run at 1,024 MB through the Apify proxy.

| Metric | Value |
|---|---|
| Status | **SUCCEEDED** |
| People resolved | **30** across **26 domains** |
| Runtime | 644.8 s |
| `chargedEventCounts` | `{ "email-resolved": 30 }` — billed == delivered |
| Fields per row | **70** |
| Fields at **100%** fill | **39** |
| Fields at **≥50%** fill | **64** |
| Confidence bands | high 9 · medium 4 · low 7 · very-low 10 |
| Match types | `pattern-inferred` 25 · `provider-heuristic` 5 |

#### Coverage — the number that matters, and it is not flattering

| Domain group | Any public address found | Address **decoded** to a pattern |
|---|---|---|
| Technology companies (11) | **11 / 11** | **10 / 11** |
| Non-technology companies (15) | **11 / 15** | **6 / 15** |
| **All 26** | **22 / 26** | **16 / 26** |

The 10 domains with no decodable evidence still shipped a full row — MX, SPF, DMARC, DKIM, RDAP and a
ranked ladder — but flagged `provider-heuristic` / structural and scored `very-low`. **That is the
correct answer, not a failure.** Read the section below before you buy.

#### What the evidence actually said

| Domain | Addresses seen | Decoded | Agreement | Top pattern | Confidence |
|---|---|---|---|---|---|
| datadoghq.com | 23 | 14 | 100% | `{first}.{last}` | 0.95 |
| sentry.io | 20 | 12 | 100% | `{first}.{last}` | 0.95 |
| shopify.com | 28 | 14 | 100% | `{first}.{last}` | 0.95 |
| twilio.com | 22 | 9 | 100% | `{f}{last}` | 0.95 |
| elastic.co | 13 | 6 | 100% | `{first}.{last}` | 0.85 |
| hashicorp.com | 33 | 6 | 100% | `{first}.{last}` | 0.85 |
| mckinsey.com | 9 | 4 | 100% | `{first}_{last}` | 0.85 |
| vercel.com | 10 | 7 | 100% | `{first}.{last}` | 0.85 |
| **mwe.com → mcdermottlaw.com** | 10 | 7 | 86% | `{f}{last}` | 0.73 |
| cloudflare.com | 45 | 15 | 73% | `{f}{last}` | 0.70 |
| jonesday.com | 6 | 4 | 75% | `{f}{last}` | 0.64 |
| kirkland.com | 7 | 4 | 50% | `{first}.{last}` | 0.43 |
| sidley.com | 109 | 10 | 40% | `{f}{last}` | 0.38 |
| stripe.com | 95 | 7 | 43% | `{first}{last}` | 0.37 |
| hubspot.com | 40 | 1 | 100% (n=1) | `{f}{last}` | 0.35 |
| ferguson.com | 1 | 1 | 100% (n=1) | `{first}.{last}` | 0.35 |

**Eight of these sixteen domains do not use `first.last`.** That is the whole argument for this actor.
And note Stripe and Sidley: a big pile of observed addresses with *low* agreement scores low, because
the domain genuinely uses several conventions — the ladder in `candidates[]` is the answer there, not
the top row.

**`mwe.com` is the case that breaks naive tools.** McDermott's website is `mwe.com`; its people's
addresses are at `mcdermottlaw.com`. This run detected that, switched `mailDomain`, set
`mailDomainDiffersFromInput: true`, and decoded 7 addresses at 86% agreement. A tool that appends the
input domain returns undeliverable `@mwe.com` guesses.

#### Badge-risk preflight (`timeout=300`, 1,024 MB)

| Input | Runs | Result |
|---|---|---|
| Prefill-only (what Apify's daily health test sends) | 3 | SUCCEEDED, 2 items each — **18.0 s / 30.0 s / 42.7 s** |
| Empty `{}` | 2 | SUCCEEDED, 2 items each — 33.4 s / 75.4 s |
| Peak platform cost on any of those runs | — | **$0.006409** |

Fleet health monitor: **PASS**, 19.1 s, 2 items.

***

### 💵 Pricing

**Pay per event — $0.002 per person resolved ($2.00 per 1,000).** One event, `email-resolved`. Both
of Apify's auto-events (`apify-default-dataset-item`, `apify-actor-start`) are removed, so your bill
shows only people you actually resolved.

- Rows are shipped with `Actor.pushData(rows, 'email-resolved')`, which is budget-aware: **delivered
  equals billed**, and a buyer who hits their charge cap never keeps rows they did not pay for.
- A person we can produce **no** candidate for is delivered **free** (pushed without the event).
- Evidence is cached per domain, so the 50th person at a company costs the same as the second.

For comparison, measured from the Apify API on 2026-09-02: the largest actor in this lane charges
**$0.029 per email resolution** (14.5× this), and a pattern-finder in the same lane bills **$0.008 per
pattern *tested*** — roughly $0.064 for one person, and you pay for every wrong guess. This actor
bills once per person and hands you the whole ladder.

***

### ⚠️ Honest limits — read these before you buy

**1. There is no SMTP verification here, and `smtpVerified` is hardcoded `false` forever.**
Outbound TCP port 25 is blocked on the Apify platform — measured 2026-09-02 from a real Apify run: MX
hosts on `:25` returned `ETIMEDOUT` in ~260 ms while `smtp.gmail.com:587` answered with a live `220`
banner from the same run. And even where port 25 *is* reachable, it lies: during recon a deliberately
garbage mailbox, `zzqqxx9988@stripe.com`, got the same `250 2.1.5 OK` from Google's MX as a real one.
Google Workspace and Microsoft 365 host most B2B mail and accept every recipient at the gateway. Any
product that calls those addresses "verified" is guessing with extra steps. We put that fact in
`catchAllBasis` on every affected row instead.

**2. Coverage is tech-skewed.** Measured above: **10 of 11** technology domains decoded, **6 of 15**
non-technology domains. GitHub commits are the single richest source and a plumbing distributor does
not have a GitHub org. Non-tech domains fall back to the website tier, and if the company publishes
no addresses at all you get `provider-heuristic` at `very-low` confidence. That is what
`cushmanwakefield.com`, `grainger.com`, `trane.com` and `wwgrainger.com` returned in this run — a
complete DNS row and an honest "we have no evidence about this domain."

**3. A correct pattern is not a live mailbox.** We prove the domain's convention. We do not prove the
person still works there, that the mailbox exists, or that they do not use a nickname — observed in
a run against the `stripe/stripe-python` repos: `David Brownman → xavdid@stripe.com` and
`Zachary Chua → zacchua@stripe.com`. Both are reported `pattern: null` rather than guessed.
`confidenceReason` says this on every row.

**4. Some sites block us, and we say so rather than reporting "nothing found."** Measured on this
build: `grainger.com` returns **HTTP 403** to the Apify container IP; `mcdermottlaw.com` (Imperva)
served a `_Incapsula_Resource` interstitial to a datacenter IP, to an Apify **RESIDENTIAL** IP *and*
to a plain home connection during testing — yet the same page returned 369,600 bytes of real content
on other attempts, so this is IP reputation and rate, not a permanent wall. Those rows carry
`blockedSources[]` naming the URL and the reason. Bring your own residential proxy via
`proxyConfiguration` if a target site refuses Apify's ranges.

**5. GitHub's unauthenticated rate limit is 60 requests/hour per IP** (measured: `X-RateLimit-Limit:
60`). Large bulk runs should supply `githubToken` for 5,000/hour. When the limit is hit the actor
degrades to the DNS + website tiers and records it in `blockedSources` — it never fails the run.

**6. `companyNameFromRegistry` filled 0/30 in this run.** ICANN privacy redacts the registrant
organisation on essentially every gTLD. The field is kept because some registries do publish it; on
`.com`/`.org`/`.io` expect `null`. Use `companyTitle` (86.7% fill) instead.

**7. Name order from a web page is not evidence.** Kirkland files Ken Au at `/lawyers/a/au-ken`, so
proximity-derived names can be surname-first. When a local part decodes under both orderings the vote
is split and both candidates appear on the ladder — which is why `kirkland.com` scores 0.43, not 0.85.

***

### ⚖️ Sources, robots and fair use

Every source this actor reads is **public and unauthenticated**: DNS records, RDAP registration data,
GitHub's public API, npm and PyPI registry metadata, and the company's own public web pages. There is
**no login, no target-site cookie, no CAPTCHA solving, no named-crawler impersonation, and no LinkedIn
scraping of any kind** — LinkedIn discovery is the practice that ended Proxycurl, and this actor takes
the name and domain from *you* instead.

The company-website tier reads `robots.txt` primarily to discover sitemaps, and the row reports
`robotsDisallowsPeoplePaths` so you can see whether a given site's `robots.txt` disallows the
people/team paths involved. Fleet policy is that a `robots.txt` `Disallow` is disclosed, not treated
as a licence question the actor decides on your behalf — the compliance call is yours.

Output is **personal work-contact data**. Depending on where you and your recipients are (GDPR,
CAN-SPAM, CASL, PECR), you may need a lawful basis to process or contact these people, and you are
responsible for that. This actor is built for legitimate B2B outreach, CRM enrichment and
deliverability research. Do not use it for spam, harassment, or to contact private individuals.

# Actor input Schema

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

The list to resolve. One object per person: `{ "firstName": "Michael", "lastName": "Broshi", "domain": "stripe.com" }`. `fullName` works instead of first/last, and `companyDomain` / `website` work instead of `domain` (a full URL is fine - it is reduced to the host). Evidence is cached per domain, so 50 people at one company cost barely more than one.

## `personLines` (type: `array`):

An alternative to the JSON list, for pasting out of a spreadsheet. One person per line, comma separated: `Michael Broshi, stripe.com` or `Michael, Broshi, stripe.com`.

## `firstName` (type: `string`):

Convenience path for resolving one person. Use with Last name + Company domain.

## `lastName` (type: `string`):

Convenience path for resolving one person. Use with First name + Company domain.

## `companyDomain` (type: `string`):

The company's domain, e.g. `stripe.com`. A full URL is accepted and reduced to the host. This is the company's domain, NOT the person's email domain - if the two differ, the actor detects it and reports `mailDomain`.

## `useGithub` (type: `boolean`):

Read the company's public GitHub org and pull the author name + address off recent public commits. This is the single richest evidence source on technology companies (measured: 56 of 100 commits on one repo carried @stripe.com addresses). It contributes nothing on a domain with no GitHub org.

## `useRegistries` (type: `boolean`):

Read public package-registry metadata (npm maintainers/contributors, PyPI author) for addresses at the company's domain.

## `useWebsite` (type: `boolean`):

Read the company's own site: homepage, robots.txt -> sitemap-discovered team / people / contact pages, and /.well-known/security.txt. This is the tier that works on non-technology companies, and the one that catches a company whose people are on a different mail domain than its website.

## `maxWebsitePages` (type: `integer`):

How many discovered team/people/contact pages to read per domain, on top of the homepage. Higher finds more evidence and costs more time. 0 reads only the homepage.

## `maxGithubRepos` (type: `integer`):

How many of the org's most recently pushed public repos to read commits from (100 commits each). Each repo is one API call against GitHub's 60-per-hour unauthenticated limit.

## `githubToken` (type: `string`):

Optional read-only GitHub personal access token. Unauthenticated GitHub allows 60 requests per hour per IP; a token raises that to 5,000, which matters on bulk runs. No scopes are needed - a token with no permissions works for public data.

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

How many ranked email candidates to return in `candidates[]`. The full ladder is what makes the row auditable: even when agreement is high, the true address is sometimes the #2 or #3 pattern.

## `maxSecondsPerDomain` (type: `integer`):

Hard cap on how long the evidence tiers may spend on one domain. When it expires the row is still emitted from what was gathered plus the DNS/RDAP profile, and `blockedSources` says the budget ran out. This exists because a single site with a 6,000-URL sitemap or a stalling page would otherwise consume the whole run.

## `maxPeople` (type: `integer`):

Safety cap on how many people from the input are resolved in one run. 0 means no cap (up to a 5,000 hard limit).

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

Applies to GitHub, npm, PyPI and company websites. DNS-over-HTTPS always runs direct - Cloudflare's resolver does not block datacenter IPs and proxying it only adds latency. Switch to RESIDENTIAL if a company site refuses datacenter IPs.

## Actor input object example

```json
{
  "people": [
    {
      "firstName": "Michael",
      "lastName": "Broshi",
      "domain": "stripe.com"
    },
    {
      "firstName": "Prathmesh",
      "lastName": "Ranaut",
      "domain": "stripe.com"
    }
  ],
  "personLines": [],
  "useGithub": true,
  "useRegistries": true,
  "useWebsite": true,
  "maxWebsitePages": 8,
  "maxGithubRepos": 3,
  "maxCandidates": 8,
  "maxSecondsPerDomain": 75,
  "maxPeople": 1000,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `people` (type: `string`):

The dataset of resolved people (one item per person in the input).

# 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": "Michael",
            "lastName": "Broshi",
            "domain": "stripe.com"
        },
        {
            "firstName": "Prathmesh",
            "lastName": "Ranaut",
            "domain": "stripe.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/work-email-finder-scraper").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": "Michael",
            "lastName": "Broshi",
            "domain": "stripe.com",
        },
        {
            "firstName": "Prathmesh",
            "lastName": "Ranaut",
            "domain": "stripe.com",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/work-email-finder-scraper").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": "Michael",
      "lastName": "Broshi",
      "domain": "stripe.com"
    },
    {
      "firstName": "Prathmesh",
      "lastName": "Ranaut",
      "domain": "stripe.com"
    }
  ]
}' |
apify call scrapersdelight/work-email-finder-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/JOhNQac8PKpWHW8W7/builds/sXZqrpk43oi0bd6qv/openapi.json
