# Website Email Scraper - Role Mailboxes from Domains (`tidytools/website-email-scraper-roles`) Actor

Scrape shared role mailboxes (info@, sales@, support@) from company websites and domain lists. One row per email with the page it was found on. Personal-looking addresses dropped; $2/1,000 sites with an email, others free.

- **URL**: https://apify.com/tidytools/website-email-scraper-roles.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 processed websites

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

## Website Email Scraper — Role Mailboxes from Domains

### What does Website Email Scraper do?

Get the shared company mailboxes (info@, sales@, support@, office@...) that businesses publish on their own public websites. Paste a list of company websites or domains into **Company websites or domains**; you get one Dataset row per mailbox with the domain, the mailbox type and the page it was found on, ready for a CRM or outreach import. The first-run form contains two public example sites; replace them with your own list.

### How to use

1. Enter one website or domain per line, e.g. `acme.com` or `https://www.acme.com/contact`.
2. Keep the defaults: **Role mailboxes only** on, **Which results are charged?** = email only, **Output format** = one row per email, **Skip pages robots.txt disallows** on, **Find websites for company names** off.
3. Start the Actor and open the **Role mailboxes** view: `domain`, `email`, `emailType`, `emailFoundOn`, `charged`, `errorType`. The full Dataset keeps the engine's other columns (phone, social profiles, source record...).

### Input example

The same values as the form defaults:

```json
{"domains":["dauntbooks.co.uk","hofbraeuhaus.de"],"roleEmailsOnly":true,"requiredContact":"email","outputMode":"emailRows","respectRobotsTxt":true,"findWebsitesForNames":false}
```

#### Other inputs

This is a focused form for the existing Website Email Extractor engine (contact-extractor). The form only changes defaults and order; it does not restrict what the engine accepts. Other valid engine inputs remain available through advanced JSON and follow the engine's own behavior and price: `startUrls`, `datasetId` (e.g. Google Maps results), `rows` from a sheet or CRM, `findWebsitesForNames: true` to look company names up, `roleEmailsOnly: false` to keep personal-looking addresses, `requiredContact: "any"` to charge websites with only a phone or social profile, and `outputMode: "sites"` for one row per website.

### Output example

A website with two role mailboxes gives two rows; only the first is charged (illustrative values, shown in the **Role mailboxes** view):

| domain | email | emailType | emailFoundOn | charged | errorType |
| --- | --- | --- | --- | --- | --- |
| acme.com | info@acme.com | role | https://acme.com/contact | true | |
| acme.com | sales@acme.com | role | https://acme.com/contact | false | |
| | | | | false | invalid_input |

The last row is a company-name line (`Acme Bakery`) with name lookup off: free, and its `input` and `error` columns in the full Dataset say why.

### Pricing

- A website where at least one role mailbox is found on its own pages is charged once (`site-processed`): $0.002 per website on Free/Bronze ($2 per 1,000), $0.0018 on Silver, $0.0016 on Gold and above. In one-row-per-email output only its first row shows `charged: true`. The price is the same however many pages are read.
- Free rows (`charged: false`): websites with only a phone number, a social profile or personal-looking addresses (dropped by **Role mailboxes only**), websites with nothing found, websites whose home page robots.txt disallows for our crawler (`robots_disallowed`), sign-in pages (`login_required`), unreachable websites, and lines that are not a website or domain, such as a company name (`invalid_input`).

Set **Max cost per run** in Apify before a large list.

### Limitations

- Only public pages of the websites you list are read (home page plus likely contact, imprint and about pages). Every request identifies itself as TidyTools; the Actor never logs in, never solves CAPTCHAs or bot checks and respects Retry-After.
- Personal-looking addresses (e.g. `anna.meier@...`) are removed before output and billing while **Role mailboxes only** is on. The Actor does not open person or social profile pages and does not guess or generate email addresses.
- robots.txt: with **Skip pages robots.txt disallows** on (the default here), pages robots.txt disallows for our crawler (TidyToolsContacts) are skipped. If you turn it off in advanced JSON, the engine reads the websites you list even where robots.txt disallows automated crawlers.
- A company name with a comma (`Acme Bakery, Austin TX`) is read as two lines, each a free `invalid_input` row.
- You are responsible for how you use the results: follow the data-protection and anti-spam laws that apply to you and the people you contact (for example GDPR, CAN-SPAM, CASL) and respect each website's terms. A shared mailbox can still reach a person; keep only what you need and honour opt-out requests.

### FAQ

**Can I enter company names instead of domains?** Not by default: a company-name line is a free `invalid_input` row. Set `findWebsitesForNames: true` in advanced JSON to let the engine look up verified websites; no verified website gives a free `company_not_found` row.

**Why is only one row per website charged?** Billing is per website, not per email: a website with several role mailboxes costs one `site-processed` event.

**Can I get personal emails too?** Set `roleEmailsOnly: false` in advanced JSON; the engine then returns every business email published on the website.

# Actor input Schema

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

Paste one company website or domain per line, e.g. acme.com or https://www.acme.com/contact. https:// is added to bare domains; paths are extra pages and www variants count once. Only shared role mailboxes (info@, sales@, support@...) published on the website are returned. Company names without a website are free invalid_input rows unless "Find websites for company names" is on. Up to 50,000 per run. Or use startUrls, datasetId or rows. The two sites are an example; replace them.

## `roleEmailsOnly` (type: `boolean`):

On (default here): only shared role mailboxes (info@, contact@, sales@, support@, office@...) are returned and used for billing; addresses that look like a named person (anna.meier@...) are dropped before output. Off: every business email published on the website is returned, including personal-looking ones.

## `requiredContact` (type: `string`):

Choose which results are charged. "email" (default here): only websites where at least one email address is kept are charged; websites with only a phone number, a social profile or a personal email dropped by "Role mailboxes only" are still returned, but free. "any": a website with at least one email, phone number or social profile is charged. The price per website is the same in both modes.

## `outputMode` (type: `string`):

"One row per email" (default here) gives each mailbox its own row with the domain, whether it is a role or personal address and the page where it was found. Each website is charged once: only its first row shows charged: true. A website with a phone or social profile but no kept email gives one row with email empty. "One row per website" puts all emails of a site in one row; the price is the same.

## `respectRobotsTxt` (type: `boolean`):

On (default here): pages robots.txt disallows for our crawler (TidyToolsContacts) are skipped, and a website whose home page is disallowed is a free robots_disallowed row. Off: the websites you list are read even where their robots.txt disallows automated crawlers, as when you open them yourself. Either way every request identifies itself as TidyTools; we never log in (sign-in pages are reported as login_required, free), never solve CAPTCHAs and respect Retry-After.

## `startUrls` (type: `array`):

The same as "Company websites or domains", as a URL list; you can also upload or link a text/CSV file of websites. Bare domains belong in the list above (this editor rejects them).

## `findWebsitesForNames` (type: `boolean`):

Boolean, false by default here: a line that is a company name rather than a website or domain is a free invalid_input row. Turn it on to look company names up as websites, e.g. {"findWebsitesForNames":true,"domains":\["Joe's Pizza, 7 Carmine St, New York, NY"]}; put a city, address or phone on the same line, or paste tab/comma-separated columns with a Name,City,Phone header. Only verified websites are used; no verified website gives a free company_not_found row.

## `namesCountry` (type: `string`):

With "Find websites for company names" on: 2-letter country code (e.g. DE, GB, JP) so that company names are first tried on that country's domains (.de, .co.uk, .co.jp) before .com. Empty = .com, .io, .co.

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

Select a dataset from an earlier run (e.g. a Google Maps or directory scraper). This Actor reads websites listed in the dataset; it does not scrape Google Maps. For Maps results, it finds the website field automatically and keeps each place's name, address, phone, Maps link and placeId (placeName, placeAddress, placePhone, mapsUrl, placeId).

## `datasetField` (type: `string`):

Field (or "rows" column) with the website or domain, e.g. "website" (dot paths such as "company.url" work). Empty = found automatically: website, then url, domain, link... A field that only holds Google Maps links (the "url" of a Maps place) is skipped, so Google Maps results need no setting.

## `datasetKeepFields` (type: `array`):

Extra fields copied from each dataset item into the output's "source" object, e.g. totalScore, reviewsCount. Google Maps results already get flat place columns (placeName, placeAddress, placePhone, mapsUrl, placeId, placeCategory, placeCity, placeCountryCode) without this setting.

## `rows` (type: `array`):

Instead of a dataset: the rows of your sheet as JSON objects, e.g. \[{"row_number": 2, "Website": "acme.com", "Email": ""}]. The website column is found automatically (Website, url, domain...) or set it in "Dataset field". Every row keeps its identifier (rowId) and gets its own output rows, so you can write the result back to the same row.

## `rowIdField` (type: `string`):

Field that identifies each row of the dataset or "rows" (returned as rowId). Empty = the first of rowId, row_number, placeId, id; otherwise the row position. Branches of a chain that share one website are read once but keep their own rowId.

## `emailField` (type: `string`):

Field with the email you already have (Email, emails, primaryEmail... are found automatically). Used by "Only rows without an email" and for "fill": the found email is only offered for rows where this field is empty, so your own values are never overwritten.

## `phoneField` (type: `string`):

Field with the phone you already have (found automatically). The found phone is only put in "fill" when it is empty.

## `onlyMissingEmail` (type: `boolean`):

With a dataset or "rows": rows that already have an email are not read and not charged (they get one row with emailStatus already_had_email).

## `dedupeStoreName` (type: `string`):

Any name, e.g. "my-lead-list". Websites (by domain) read in an earlier run with the same name are not read or charged again: the earlier result is returned with fromPreviousRun. Kept in a key-value store "contacts-<name>" in your Apify account. Failed websites are not kept, so they are tried again. Empty = off.

## `dedupeMaxAgeDays` (type: `integer`):

With a dedupe name: results older than this are read again. 0 = no age limit.

## `maxPagesPerSite` (type: `integer`):

The home page plus likely contact pages (contact, imprint/Impressum, about, team, support, legal). When those show no email, pages left over go to other pages likely to name a mailbox (policies, catering, jobs, locations...). The price per website is the same however many pages are read.

## `excludePaths` (type: `array`):

Optional JSON array of site paths beginning with /, e.g. {"excludePaths":\["/blog","/privacy"]}; enter one per line in the form. /blog excludes /blog/... but not /blogger; matching ignores case and query strings. Home pages and URLs you explicitly give are still read. Empty means no exclusion. A non-path entry causes a free invalid_input row and stops reading.

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

How many websites are processed at the same time (each reads up to 2 pages at once).

## `siteTimeoutSecs` (type: `integer`):

After this time no further pages of a website are started and the pages read so far are returned (skipped pages are listed in timedOutPages). If even the home page is not read by then, the website fails (not charged).

## `httpVia` (type: `string`):

Some sites block requests from data centers. Auto retries from a second network before falling back to a real browser. If those routes are refused, Auto also tries our second server (Oracle, different IP) before a real browser.

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

Only used for requests sent from Apify's network. Apify proxy usage is billed to your Apify account.

## Actor input object example

```json
{
  "domains": [
    "dauntbooks.co.uk",
    "hofbraeuhaus.de"
  ],
  "roleEmailsOnly": true,
  "requiredContact": "email",
  "outputMode": "emailRows",
  "respectRobotsTxt": true,
  "findWebsitesForNames": false,
  "onlyMissingEmail": false,
  "dedupeMaxAgeDays": 90,
  "maxPagesPerSite": 8,
  "maxConcurrency": 10,
  "siteTimeoutSecs": 120,
  "httpVia": "auto"
}
```

# Actor output Schema

## `mailboxes` (type: `string`):

No description

## `full` (type: `string`):

No description

## `failed` (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 = {
    "domains": [
        "dauntbooks.co.uk",
        "hofbraeuhaus.de"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/website-email-scraper-roles").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 = { "domains": [
        "dauntbooks.co.uk",
        "hofbraeuhaus.de",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tidytools/website-email-scraper-roles").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 '{
  "domains": [
    "dauntbooks.co.uk",
    "hofbraeuhaus.de"
  ]
}' |
apify call tidytools/website-email-scraper-roles --silent --output-dataset

```

## MCP server setup

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

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/XoMGiATCsXyfI7N8z/builds/kgFsvibhOix53m2QH/openapi.json
