# B2B Contact & Email Finder — Decision Makers by Domain (`brilliant_gum/b2b-contact-email-finder`) Actor

Turn a company domain into named decision makers: full name, job title, seniority, department, LinkedIn URL and a work email built from the person's name, with a live DNS/MX check and full company firmographics attached to every contact.

- **URL**: https://apify.com/brilliant\_gum/b2b-contact-email-finder.md
- **Developed by:** [Yuliia Kulakova](https://apify.com/brilliant_gum) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.02 / contact

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## B2B Contact & Email Finder

![B2B Contact & Email Finder](https://api.apify.com/v2/key-value-stores/IuFu0Cw6PpOkNkDV7/records/b2b-contact-email-finder.png)

**Give it a company domain, get named decision makers back.** Full name, job title,
seniority, department, LinkedIn URL and company firmographics — plus a work-email address
for each person, with the domain's mail setup checked live and the exact evidence attached
to every record.

***

### What it does

Point the actor at one or more company domains. For each one it:

1. **Reads the company's own public pages**, and falls back to a **real browser** for the
   ones that are rendered by JavaScript.
2. **Extracts named people** — full name, job title, seniority, department and any publicly
   linked LinkedIn / social profiles — plus **company firmographics** (founded year, HQ,
   phone, tech stack, description, social profiles).
3. **Builds the work email when it isn't published.** For a named person with no address on
   the page it generates the likely work-email patterns (`{first}.{last}@domain`, …), picks
   the highest-priority one, and records which pattern produced it in `emailPattern`.
4. **Checks the address** — syntax, role account, disposable domain and a live DNS/MX lookup
   of the domain — and returns a status, a 0-100 confidence score, the raw evidence and a
   timestamp.

Built for **sales teams** doing targeted outbound, **recruiters** sourcing named people at
specific companies, and **agencies** building or cleaning contact lists for clients.

It works from public company websites plus generated patterns — it does **not** scrape
LinkedIn and does **not** resell a third-party contact database.

***

### What "checked" means here — read this before you buy

Email checking has levels, and this actor tells you exactly which level each record got.

| Check | What it proves | Runs on Apify |
|---|---|---|
| Syntax | the address is well formed | ✅ always |
| Role account | `info@`, `sales@` … — a shared inbox, not a person | ✅ always |
| Disposable domain | throwaway mail provider | ✅ always |
| **Live DNS / MX lookup** | **the domain really accepts mail, and through which server** | ✅ always |
| Mailbox probe (SMTP `RCPT TO`) | that one specific mailbox exists | ❌ needs outbound port 25 |

**The mailbox probe does not run on Apify.** Apify — like almost every cloud provider —
filters outbound port 25, so no actor running there can complete an SMTP handshake with a
mail server. The code for it is in the actor and switches on by itself wherever the port is
open (`emailVerification: "full"`), but on the platform it detects the block, writes
`smtpBlocked: true` into the record and falls back to the MX check. **It never invents a
verdict it could not confirm.**

So what you get per person is: a real name, a real title, and a **pattern-built work
address on a domain that is confirmed to accept mail** — reported as `emailStatus:
"unknown"` with `emailConfidence: 50`, because that is what was actually established. If a
tool promises you a per-mailbox `valid` verdict from inside a cloud platform, ask it which
port it used.

Addresses that are genuinely published on the company site are returned as found, not
guessed — the `source` field tells you which is which.

***

### Key features

#### Per-record evidence, not a black-box score

- **`emailStatus`** — on this platform you will see `unknown` (domain accepts mail, mailbox
  not provable), `role`, `disposable` or `invalid`. The values `valid` and `catch-all` exist
  in the code but require the mailbox probe, so they do not occur on Apify.
- **`emailConfidence`** — 0-100. What you will actually see: MX confirmed = 50, role = 40,
  disposable = 8, invalid or no MX = 0-3. (`catch-all` = 55 and a probed mailbox = 95 belong
  to the mailbox probe.)
- **`emailVerification`** — `mode`, `mxFound`, `mxHost`, `smtpChecked`, `smtpBlocked`,
  `smtpCode`, `catchAll`, `disposable`, `role` and a human-readable `note`.
- **`lastVerifiedAt`** — ISO-8601 stamp for freshness.

#### Browser-rendered team-page extraction

When the plain HTTP crawl finds zero people (JS-rendered leadership/team pages), a real
browser renders them and re-runs the same extractors. Controlled by `renderJs`
(`auto` = only when needed, `always`, `off`).

#### Company firmographics

Every contact carries a nested `company` object: name, website, industry, size, revenue
range, founded year, HQ address, phone, LinkedIn, social profiles, detected **tech stack**
and description.

#### No invented people, and no address that implies a job someone doesn't have

Every name and title comes off the page as written — nothing is inferred. People who appear
on a company page but are **not employees** — board members, investors, advisors, partners
at other firms — are returned with their real title and `affiliation: "board/external"`, and
their company address is left `null` on purpose rather than built from a pattern.

The work address itself is either **found** on the site or **built from the person's name**,
and the record always says which: `emailPattern` names the pattern used, and `source` says
where the person came from (`<domain> (team page)`, `<domain> (json-ld)`, `<domain> (site)`,
`<domain> (role-probe)`, `apollo (…)`).

#### Word-accurate title filtering, and titles that carry two hats

Seniority and department are matched on whole words. Asking for `CTO` returns CTOs — not
every *Dire**cto**r*; asking for `COO` does not sweep in every ***Coo**rdinator*.

A title often belongs to more than one bucket: "CEO & Co-Founder" is both `c_suite` and
`founder`, "Chief Product & Technology Officer" is both `product` and `engineering`. Every
matching label is kept in `seniorityAll` / `departmentAll`, and your `seniorities` /
`departments` filters match against all of them — so filtering for the C-suite returns the
founder-CEO instead of quietly dropping them. `seniority` and `department` stay
single-valued for convenience and hold the primary label.

#### DNC / suppression list

`suppressionList` drops any email or whole domain you paste in — a simple opt-out / GDPR
do-not-contact control applied before results are written.

#### Role / shared-inbox separation

Generic inboxes (`info@`, `sales@`, `support@`, …) are **never** counted as people. The
opt-in `roleEmailProbe` writes them to a **separate `role-inboxes` dataset**, so they never
inflate your contact count and are never billed as contacts.

#### Optional: Apollo firmographic search (bring-your-own-key)

If you need free-text firmographic **people search with no seed domain** (industry / title /
seniority / headcount at scale), set `mode: "search"` and supply **your own** Apollo API key
(`apolloApiKey`). The query runs on your Apollo account, quota and terms; Apollo returns the
people and firmographics, and this actor builds and checks the emails. In this mode
`domains` and `companyNames` act as employer **filters** on the Apollo query — they are not
also crawled, so a search run never quietly bills you for a site crawl you did not ask for.
This is a secondary add-on — **most users should use the default domain mode**, which needs
no external key.

***

### Input

The default and recommended mode is `domainToContacts`. Minimal example:

```json
{
  "mode": "domainToContacts",
  "domains": ["hubspot.com", "datadoghq.com"],
  "jobTitles": ["VP Marketing", "Head of Sales", "CFO"],
  "emailVerification": "mx",
  "requireEmail": true,
  "maxResults": 50,
  "maxPagesPerDomain": 12,
  "renderJs": "auto",
  "suppressionList": ["optout@example.com", "competitor.com"],
  "proxyConfiguration": { "useApifyProxy": true }
}
```

#### Core

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | enum `domainToContacts` | `search` | `domainToContacts` | Pipeline mode. `domainToContacts` crawls the domains you give it; `search` is the optional Apollo/firmographic path. |
| `domains` | array\<string> | `["hubspot.com"]` | Company domains to mine (e.g. `hubspot.com`). In `search` mode, an employer filter. |
| `companyNames` | array\<string> | `[]` | Company name filter / seed. |
| `personName` | string | `""` | Filter or seed by a specific person's name. |
| `maxResults` | integer | `25` | Maximum contacts to output. |

#### Contact filters & output control

| Field | Type | Default | Description |
|---|---|---|---|
| `jobTitles` | array\<string> | `[]` | Keep people whose title contains any of these as a **whole word** (case-insensitive). |
| `excludeJobTitles` | array\<string> | `[]` | Drop people whose title matches any of these. |
| `seniorities` | array\<enum> | `[]` | `founder`, `c_suite`, `partner`, `vp`, `head`, `director`, `manager`, `senior`, `entry`. |
| `departments` | array\<enum> | `[]` | `executive`, `engineering`, `product`, `sales`, `marketing`, `finance`, `hr`, `operations`, `legal`, `it`. |
| `locations` | array\<string> | `[]` | Include locations (city / state / country substrings). |
| `excludeLocations` | array\<string> | `[]` | Exclude locations. |
| `keywords` | array\<string> | `[]` | Include keywords found on company pages. |
| `excludeKeywords` | array\<string> | `[]` | Exclude keywords. |
| `matchLogic` | enum `AND` | `OR` | `AND` | Combine the filters above with AND or OR. |
| `requireEmail` | boolean | `true` | Only return contacts with a non-invalid email. |
| `requirePhone` | boolean | `false` | Only return contacts with a personal phone number. The site crawl yields *company* numbers, not per-person ones — with this on, `domainToContacts` runs usually return nothing. Useful mainly in Apollo `search` mode. |
| `dedup` | boolean | `true` | Deduplicate by email + LinkedIn URL. |
| `suppressionList` | array\<string> | `[]` | Emails or domains to always drop (opt-out / DNC). |

#### Email verification

| Field | Type | Default | Description |
|---|---|---|---|
| `emailVerification` | enum `off` | `mx` | `full` | `mx` | `off` = syntax / role / disposable only, no network; `mx` = also a live DNS/MX lookup; `full` = additionally attempt a mailbox probe, which needs outbound port 25 and falls back to `mx` automatically where the port is filtered (it is on Apify). |
| `emailPatterns` | array\<string> | 8 defaults | Patterns generated for named people. Tokens: `{first}` `{last}` `{f}` `{l}` `{domain}`. |
| `smtpTimeoutMs` | integer | `8000` | Timeout per SMTP connection. Only used by `emailVerification: "full"`; where port 25 is filtered this is how long each blocked attempt waits before falling back. |
| `phoneEnrichment` | boolean | `false` | Also collect phone numbers found on the company site into `company.phone`. |
| `roleEmailProbe` | boolean | `false` | Opt-in. Also check common shared inboxes (`info@`, `sales@`, …). Written to a **separate** `role-inboxes` dataset; never counted as people and never billed. |

#### Crawl & rendering

| Field | Type | Default | Description |
|---|---|---|---|
| `maxPagesPerDomain` | integer | `12` | Crawl budget per company domain. |
| `renderJs` | enum `auto` | `always` | `off` | `auto` | Browser fallback for JS-rendered team pages. `auto` renders only when the HTTP crawl finds 0 people. |
| `proxyConfiguration` | object | `{ "useApifyProxy": true }` | Proxy for the site crawl and the browser fallback. Residential works best on protected sites. |

#### Firmographic search filters (`search` mode)

| Field | Type | Default | Description |
|---|---|---|---|
| `domainMatch` | enum `strict` | `contains` | `strict` | How to match domains in `search` mode. |
| `industries` | array\<string> | `[]` | Industry filter. |
| `headcountRanges` | array\<enum> | `[]` | `1-10` … `10001+`. |
| `revenueRanges` | array\<enum> | `[]` | `<1M` … `>1B`. |
| `fundingStage` | array\<enum> | `[]` | `bootstrapped`, `pre_seed`, `seed`, `series_a` … `series_e_plus`, `ipo`, `acquired`. |
| `techStack` | array\<string> | `[]` | Required technologies. |

#### Apollo (optional, bring-your-own-key)

| Field | Type | Default | Description |
|---|---|---|---|
| `apolloApiKey` | string (secret) | `""` | **Your own** Apollo master API key. Enables free-text firmographic people search with no seed domain. Runs on your Apollo account / quota / ToS. |
| `apolloSearchUrl` | string | `""` | Optional. Paste an Apollo people-search URL; its filters are parsed into the search. Requires `apolloApiKey`. |
| `apolloRevealEmails` | boolean | `false` | Optional. Unlock each email via Apollo enrichment (**consumes your own Apollo credits**). Off = emails are built from name patterns and checked against DNS/MX at no extra cost. |

***

### Output

One flat record per person, with a nested `company` object. Below is a **real record**
from a run against `hubspot.com` in `mx` mode, trimmed only by dropping the empty fields.
This address was **not published on the site** — it was built from `{first}.{last}@{domain}`,
and what was confirmed is that the domain accepts mail. The record says exactly that.

```json
{
  "recordType": "person",
  "fullName": "Yamini Rangan",
  "firstName": "Yamini",
  "lastName": "Rangan",
  "jobTitle": "Chief Executive Officer",
  "seniority": "c_suite",
  "seniorityAll": ["c_suite"],
  "department": "executive",
  "departmentAll": ["executive"],
  "affiliation": null,
  "companyName": "HubSpot",
  "companyDomain": "hubspot.com",
  "linkedinUrl": "https://www.linkedin.com/in/yaminirangan",
  "email": "yamini.rangan@hubspot.com",
  "emailStatus": "unknown",
  "emailConfidence": 50,
  "emailPattern": "{first}.{last}@{domain}",
  "emailVerification": {
    "mode": "mx",
    "mxFound": true,
    "mxHost": "smtp.google.com",
    "smtpChecked": false,
    "smtpBlocked": false,
    "smtpCode": null,
    "catchAll": null,
    "disposable": false,
    "role": false,
    "note": "MX found; SMTP not attempted (mx mode)"
  },
  "lastVerifiedAt": "2026-09-09T14:43:54.515Z",
  "source": "hubspot.com (team page)",
  "company": {
    "name": "HubSpot",
    "domain": "hubspot.com",
    "website": "https://hubspot.com",
    "size": "5001-10000 employees",
    "foundedYear": "2006",
    "hq": "2 Canal Park, Cambridge, MA, 02141, US",
    "phone": "+1888HUBSPOT",
    "linkedinUrl": "https://www.linkedin.com/company/hubspot",
    "techStack": ["Google Analytics", "HubSpot", "Cloudflare", "Google Fonts", "Webflow"],
    "description": "HubSpot is an AI-powered customer platform with software for marketing, sales, customer service, content management, operations, and commerce.",
    "dataFreshness": "2026-09-09T14:43:54.523Z"
  }
}
```

#### When the mailbox probe is blocked

Running `emailVerification: "full"` on a network that filters port 25 — which includes the
Apify platform — produces this, a real captured object. Note that it reports the block
instead of returning a verdict:

```json
"emailStatus": "unknown",
"emailConfidence": 50,
"emailVerification": {
  "mode": "full",
  "mxFound": true,
  "mxHost": "aspmx.l.google.com",
  "smtpChecked": false,
  "smtpBlocked": true,
  "smtpCode": null,
  "catchAll": null,
  "disposable": false,
  "role": false,
  "note": "SMTP unreachable (ETIMEDOUT); fell back to MX-only"
}
```

Because the fallback is automatic and the outcome is the same as `mx`, running `full` on
Apify only costs you the timeout per domain. **Use `mx`.**

#### No company address for people who don't work there

Board members, investors and partners at other firms are returned with their real title and
no company address — like this:

```json
{
  "fullName": "Shardul Shah",
  "jobTitle": "Partner, Index Ventures",
  "companyName": "Datadog",
  "email": null,
  "emailStatus": null,
  "emailConfidence": null,
  "affiliation": "board/external",
  "source": "datadoghq.com (team page)"
}
```

**Field reference** — person: `recordType`, `fullName`, `firstName`, `lastName`,
`jobTitle`, `headline`, `seniority`, `seniorityAll`, `department`, `departmentAll`,
`affiliation`, `affiliationReason`,
`companyName`, `companyDomain`, `linkedinUrl`, `email`, `emailStatus`, `emailConfidence`,
`emailPattern`, `emailVerification`, `lastVerifiedAt`, `personalEmail`, `phone`, `mobile`,
`city`, `state`, `country`, `socials[]`, `source`. Company: `company.{ name, domain,
website, industry, size, revenueRange, totalFunding, marketCap, foundedYear, hq, phone,
linkedinUrl, socials[], techStack[], description, dataFreshness }`.

***

### Pricing

You pay for contacts, not for runtime. The full price list is two entries:

| Event | When it is charged |
|---|---|
| **`lead-returned`** | One contact record written to your dataset — the person, their title and labels, LinkedIn URL, work address with its evidence, and the whole company object. |
| `apify-actor-start` | Once when a run starts (Apify charges one of these per GB of the memory you run with). |

What is deliberately **not** on that list:

- **Board members, investors and partners at other firms** are returned for context but are
  **not charged as contacts**.
- **Role / shared inboxes** (`info@`, `sales@` …) go to a separate dataset and are **not
  charged** either.
- **Email checking is not billed separately.** A separate "email verified" charge would be
  billing you for a mailbox probe that cannot run on this platform, so there isn't one.
- **Phone numbers are not billed separately.** In Apollo mode the number came out of your
  own Apollo credits; charging again for it would be charging twice.

Set `maxResults` to cap what a single run can cost you.

***

### Use cases

- **Targeted outbound to specific companies** — you already have an account list; feed the
  domains in and get named decision makers with work addresses for your sequencer.
- **Account-based marketing (ABM)** — map the buying committee at each target account
  (title + seniority + department) with company firmographics attached.
- **Recruiting / sourcing** — find named people at specific target employers without
  scraping LinkedIn.
- **List enrichment** — attach titles, seniority, department, LinkedIn URLs and
  firmographics to a list of domains you already have.

***

### Honest limitations

- **No per-mailbox verification on this platform.** Outbound port 25 is filtered on Apify,
  so the SMTP `RCPT TO` probe cannot complete and no `valid` / `invalid` per-mailbox verdict
  is produced here. You get syntax, role, disposable and a live MX check. If you need a
  per-mailbox verdict, run the output through a dedicated verification service before you
  send. The actor is explicit about this in every record rather than dressing a guess up as
  a confirmation.
- **A pattern-built address is a best guess on a confirmed domain.** The pattern used is in
  `emailPattern`, and `source` tells you whether the address was found on the site or built.
- **Catch-all domains can't be confirmed per mailbox by anyone.** Where detectable they are
  returned as `catch-all` (confidence 55), flagged honestly, not as `valid`.
- **Coverage depends on the public site.** People and titles come from the company's own
  team/leadership/about pages. Some companies deliberately publish none — Stripe is one:
  it has no leadership page at all, so a run against `stripe.com` returns firmographics and
  zero named people. That is the site being empty, not the actor failing, and the log says
  so explicitly.
- **Firmographic people-search without a seed domain needs your own Apollo key.** The
  default domain mode needs no external key and is what most users should use.
- **No LinkedIn scraping.** LinkedIn URLs are surfaced only when publicly linked on the
  site; the actor does not log in to or scrape LinkedIn.

***

### FAQ

**Where do the emails come from?**
From the company's own site (`mailto:` links, contact/team pages) and, for named people
without a published address, from generated name patterns. Nothing is bought or resold from
a third-party database.

**Is the email verified?**
The *domain* is verified live — MX lookup, plus syntax, role and disposable checks. The
individual *mailbox* is not, because that requires outbound port 25, which this platform
filters. Each record states exactly which checks ran.

**What does `emailConfidence` mean?**
A 0-100 score derived from the evidence actually collected: catch-all domain = 55, MX
confirmed but no mailbox probe = 50, role account = 40, disposable = 8, invalid or no MX =
0-3. The raw evidence sits in `emailVerification`, so you can set your own threshold.

**Should I use `emailVerification: "full"`?**
Not on Apify. It attempts a mailbox probe, waits out the blocked connection per domain and
then falls back to `mx` anyway — same result, slower. Use `mx`.

**Do board members and shared inboxes count as contacts?**
No. External/board people are returned with `affiliation: "board/external"` and no company
email; role inboxes (opt-in) go to a separate `role-inboxes` dataset. Neither is billed.

**Do I need an Apollo key?**
No — the default `domainToContacts` mode needs no external key. An Apollo key only unlocks
the optional free-text firmographic `search` mode.

**Is this compliant for cold outreach?**
The actor supports responsible use: a `source` field for provenance, a `suppressionList`
for opt-out/DNC, and field minimization. B2B cold email is generally defensible under
legitimate interest (GDPR Art. 6(1)(f)), but you are the data controller — keep a
documented basis and honour opt-outs.

# Actor input Schema

## `mode` (type: `string`):

domainToContacts = crawl company domains and build verified contacts. search = firmographic search (resolves supplied domains/companies into the contact pipeline; free-text firmographic querying needs the firmographic index, documented in SPEC).

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

Company domains to mine (domainToContacts) or to use as an employer filter (search). One per line, e.g. hubspot.com. Note: some companies (Stripe, for example) deliberately publish no team or leadership page — for those the actor returns firmographics but no named people, because there are none to read.

## `domainMatch` (type: `string`):

How to match domains in search mode.

## `jobTitles` (type: `array`):

Include people whose title matches any of these (case-insensitive substring). e.g. VP Marketing, Head of Sales.

## `excludeJobTitles` (type: `array`):

Drop people whose title matches any of these.

## `seniorities` (type: `array`):

Filter by seniority level. A title can hold several levels at once ("CEO & Co-Founder" is both c\_suite and founder) — the filter matches any of them, and all labels are returned in `seniorityAll`.

## `departments` (type: `array`):

Filter by department. A title can span several departments — the filter matches any of them, and all labels are returned in `departmentAll`.

## `personName` (type: `string`):

Filter/seed by a specific person name.

## `companyNames` (type: `array`):

Company name filter/seed.

## `industries` (type: `array`):

Industry filter (search mode).

## `locations` (type: `array`):

Include locations (city/state/country substrings).

## `excludeLocations` (type: `array`):

Exclude locations.

## `headcountRanges` (type: `array`):

Company size filter (search mode).

## `revenueRanges` (type: `array`):

Company revenue filter (search mode).

## `fundingStage` (type: `array`):

Company funding stage filter (search mode).

## `techStack` (type: `array`):

Required technologies (search mode).

## `keywords` (type: `array`):

Include keywords found on company pages.

## `excludeKeywords` (type: `array`):

Exclude keywords.

## `matchLogic` (type: `string`):

Combine multiple filters with AND or OR.

## `requireEmail` (type: `boolean`):

Only return contacts whose email passed the checks above (drops invalid addresses).

## `requirePhone` (type: `boolean`):

Only return contacts that have a personal phone number. Note: the site crawl yields company phone numbers (company.phone), not per-person ones — with this on, domainToContacts runs usually return nothing. Useful mainly in Apollo search mode.

## `emailVerification` (type: `string`):

How hard to check each address. off = syntax, role and disposable checks only, no network. mx = also a live DNS/MX lookup of the domain (does the domain accept mail at all). full = additionally attempt a mailbox-level SMTP handshake; this needs outbound port 25, which most cloud networks filter, and it falls back to mx automatically when the port is blocked. Every record reports which checks actually ran in its emailVerification object.

## `phoneEnrichment` (type: `boolean`):

Also collect phone numbers found on the company site into company.phone.

## `dedup` (type: `boolean`):

Deduplicate contacts by email + linkedinUrl.

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

Emails or domains to always drop (GDPR opt-out / do-not-contact).

## `maxResults` (type: `integer`):

Maximum number of contacts to output. Each contact returned is one billed record.

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

Crawl budget per company domain.

## `roleEmailProbe` (type: `boolean`):

OFF by default. When enabled, also verifies common shared inboxes (info@, sales@, support@, ...). These are NEVER counted as people and NEVER inflate the contact count — they are written to a SEPARATE 'role-inboxes' dataset with recordType='roleInbox'.

## `renderJs` (type: `string`):

For domainToContacts. Many sites render team/about/leadership pages with JavaScript, so the HTTP crawl finds no people. 'auto' launches a real browser only when the HTTP crawl finds 0 named people; 'always' always renders; 'off' disables it.

## `apolloApiKey` (type: `string`):

Your OWN Apollo.io master API key. Enables free-text firmographic PEOPLE search in mode=search (industry/title/seniority/size with NO seed domain). The query runs on your Apollo account/quota/ToS. Apollo returns people + firmographics; their emails are then pattern-generated and live-verified by this actor. Get a key at Apollo -> Settings -> Integrations -> API Keys.

## `apolloSearchUrl` (type: `string`):

Optional. Paste an Apollo people-search URL (app.apollo.io/#/people?...) built in Apollo's UI; its filters are parsed into the search. Requires apolloApiKey.

## `apolloRevealEmails` (type: `boolean`):

Optional. Unlocks each person email through Apollo People Enrichment, which CONSUMES YOUR OWN APOLLO CREDITS. Off by default: emails are built from name patterns and checked against DNS/MX at no extra cost.

## `emailPatterns` (type: `array`):

Patterns to generate for named people. Tokens: {first} {last} {f} {l} {domain}.

## `smtpTimeoutMs` (type: `integer`):

Timeout per SMTP connection, used only by emailVerification = full. On a network that filters port 25 this is how long each blocked attempt waits.

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

Proxy for the site crawl and the browser fallback. Residential works best on protected sites.

## Actor input object example

```json
{
  "mode": "domainToContacts",
  "domains": [
    "hubspot.com"
  ],
  "domainMatch": "strict",
  "jobTitles": [],
  "excludeJobTitles": [],
  "seniorities": [],
  "departments": [],
  "personName": "",
  "companyNames": [],
  "industries": [],
  "locations": [],
  "excludeLocations": [],
  "headcountRanges": [],
  "revenueRanges": [],
  "fundingStage": [],
  "techStack": [],
  "keywords": [],
  "excludeKeywords": [],
  "matchLogic": "AND",
  "requireEmail": true,
  "requirePhone": false,
  "emailVerification": "mx",
  "phoneEnrichment": false,
  "dedup": true,
  "suppressionList": [],
  "maxResults": 25,
  "maxPagesPerDomain": 12,
  "roleEmailProbe": false,
  "renderJs": "auto",
  "apolloSearchUrl": "",
  "apolloRevealEmails": false,
  "emailPatterns": [
    "{first}.{last}@{domain}",
    "{first}@{domain}",
    "{f}{last}@{domain}",
    "{first}{last}@{domain}",
    "{f}.{last}@{domain}",
    "{first}_{last}@{domain}",
    "{last}@{domain}",
    "{first}{l}@{domain}"
  ],
  "smtpTimeoutMs": 8000,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `contacts` (type: `string`):

No description

## `roleInboxes` (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 = {
    "mode": "domainToContacts",
    "domains": [
        "hubspot.com"
    ],
    "emailVerification": "mx",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("brilliant_gum/b2b-contact-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 = {
    "mode": "domainToContacts",
    "domains": ["hubspot.com"],
    "emailVerification": "mx",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("brilliant_gum/b2b-contact-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 '{
  "mode": "domainToContacts",
  "domains": [
    "hubspot.com"
  ],
  "emailVerification": "mx",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call brilliant_gum/b2b-contact-email-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,brilliant_gum/b2b-contact-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/uRAMN2lvASQGWasgR/builds/nV7fcMgRarVt80NJ9/openapi.json
