# Domain List Email Extractor: Emails & Phones from Websites (`data-done-right/domain-email-extractor`) Actor

Domain List Email Extractor API: extract emails from websites in bulk. An email extractor from domains: reads the homepage, contact, about and team pages and returns emails (role or personal), E.164 phones, contact page and social links. One row per domain; pay per domain with contacts.

- **URL**: https://apify.com/data-done-right/domain-email-extractor.md
- **Developed by:** [Data Done Right](https://apify.com/data-done-right) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.28 / 1,000 domains

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

## Domain List Email Extractor: Emails & Phones from Websites

This **Domain List Email Extractor** takes a list of domains or website URLs and returns the **emails from websites** they publish, plus phone numbers, the contact page and social profile links. It is an **email extractor from domains**: for each site it reads the homepage and a few contact, about, team, careers or imprint pages (found through the site's own links and common paths such as `/contact`), and gives you **one row per domain**, in the same order as your list. No browser, no login, no API key.

It reads what a site publishes in its HTML and nothing else. It does not guess addresses, does not verify mailboxes and does not send email. See [Responsible use](#responsible-use) and [Honest limits](#honest-limits).

### How to use the domain list email extractor

1. Paste your domains or URLs into **Domains or website URLs**, one per line (`acme.com`, `https://www.example.org/contact`). URLs are cut back to the site root.
2. Choose how many pages to read per domain (default 5) and whether you want phone numbers, role emails (`info@`) and personal emails.
3. Run it. Download one row per domain as JSON, CSV or Excel, or read it through the Apify API.

Every input gets a row, even when nothing is found, so your list stays aligned (the one exception: if a spending limit stops the run early, domains still waiting are not read and get no row). Use `inputIndex` or the `input` field to join the result back to your sheet.

### Extract emails from websites: what you get

| Field | Meaning |
|---|---|
| `status` | `found`, `not_found`, `blocked`, `unreachable` or `invalid` (see below) |
| `emails` | `{address, type, sourceUrl, onDomain}`: `type` is `role` (info@, sales@, support@, hello@, press@...) or `personal` (jane.doe@), `sourceUrl` is the page it was published on |
| `phones` | `{e164, raw, sourceUrl}`: valid numbers only, normalised offline to E.164 (`+14155552671`), duplicates merged |
| `contactPageUrl` | the domain's contact page, when there is one |
| `socialLinks` | profile links by platform (facebook, linkedin, twitter, instagram, youtube, tiktok, github...); share buttons are ignored |
| `pagesChecked` | pages requested on the domain |
| `emailAddresses`, `phoneNumbers`, `emailCount`, `phoneCount` | flat versions of the above for CSV and spreadsheets |
| `reason` | plain-language explanation of the status |

| Status | Meaning | Charged |
|---|---|---|
| `found` | At least one email or phone number was found. | yes |
| `not_found` | The site was read, but its HTML publishes no email or phone number (the reason says when it looks JavaScript-rendered). | no |
| `blocked` | `robots.txt` forbids reading, the site refuses bots (HTTP 401/403/429/503, bot-check page) or the domain points to an internal address. | no |
| `unreachable` | No usable answer: DNS failure, timeout, connection error, HTTP 404/5xx. | no |
| `invalid` | Not read: free-mail (gmail.com), social network or large platform, internal name or IP, not a domain, a duplicate of an earlier input, or beyond your `maxDomains` cap. | no |

### Email extractor from domains: what it reads

- **Pages:** the homepage, then links that look like contact, about, team, people, careers, press or imprint pages (also `kontakt`, `contacto`, `impressum`, `mentions-legales`), then common paths (`/contact`, `/about`, `/team`, `/impressum`...). Blog, shop and docs pages are skipped. Guessed paths stop after two 404s.
- **Where emails come from:** visible text, `mailto:` links (including percent-encoded ones), JSON-LD data in the page. These are always read.
- **Hidden addresses (Decode hidden addresses, on by default):** `name [at] site [dot] com`, `(at)` / `(dot)`, `{at}`, HTML entities such as `&#64;`, full-width `＠`, and **Cloudflare email protection** (`data-cfemail` and `/cdn-cgi/l/email-protection` links).
- **Placeholders are dropped:** `you@example.com`, `name@domain.com`, image file names such as `logo@2x.png`, tracking addresses.
- **Phones:** `tel:` links and numbers in the text, parsed with the offline `phonenumbers` library. Numbers written without `+country code` are read in the country of the domain's ending (`.de`, `.co.uk`) or the **Default phone country** you set, otherwise US. Order numbers, dates and IDs are not returned.
- **Roles vs people:** a mailbox such as `info`, `sales`, `support`, `hello`, `press`, `careers`, `billing` or `team` is `role`; everything else is `personal`. The check is by name, so a shared mailbox with an unusual name shows up as `personal`.
- **Limits per domain:** up to 50 emails and 20 phone numbers.

### Emails from a list of domains: input example

```json
{
  "domains": ["apify.com", "https://www.python.org/about/", "example.com", "gmail.com"],
  "pagesPerDomain": 5,
  "extractPhones": true,
  "includeRoleEmails": true,
  "includePersonalEmails": true,
  "decodeObfuscated": true,
  "concurrency": 10
}
```

Besides the options above: `maxDomains` (hard cap, default 1000; domains beyond it get a free `invalid` row), `onlyOwnDomainEmails` (drop gmail.com and partner addresses), `defaultPhoneCountry` (`"DE"`), `delaySecs` (default 1), `timeoutSecs` (default 10) and an optional `proxyConfiguration`.

### Output example

A trimmed row from a real run on the example input (sites change their pages, so your result will differ):

```json
{
  "inputIndex": 0, "input": "apify.com", "domain": "apify.com", "status": "found",
  "reason": "Found contact details on 5 page(s).",
  "emails": [
    {"address": "support@apify.com", "type": "role", "sourceUrl": "https://apify.com/contact", "onDomain": true},
    {"address": "hello@apify.com", "type": "role", "sourceUrl": "https://apify.com/contact", "onDomain": true}
  ],
  "phones": [],
  "emailAddresses": ["support@apify.com", "hello@apify.com"], "phoneNumbers": [], "emailCount": 2, "phoneCount": 0,
  "pagesChecked": 5, "contactPageUrl": "https://apify.com/contact",
  "socialLinks": {"github": "https://github.com/apify/crawlee", "linkedin": "https://linkedin.com/company/apify",
                  "twitter": "https://x.com/apify", "youtube": "https://youtube.com/apify", "tiktok": "https://tiktok.com/@apifytech"},
  "charged": true
}
```

A `SUMMARY` record in the key-value store counts domains by status, charged domains and whether a spending limit stopped the run.

### Domain List Email Extractor API

Call it from the Apify API, Python, JavaScript, Make, Zapier or n8n. Example with the Apify Python client:

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("data-done-right/domain-email-extractor").call(run_input={
    "domains": ["acme.com", "https://www.example.org/contact"], "pagesPerDomain": 5})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["domain"], row["status"], row["emailAddresses"], row["phoneNumbers"])
```

Join the rows back to your list with `inputIndex`. Rows with `status` other than `found` are free and tell you why in `reason`.

### Use it in Claude, ChatGPT and Cursor

Connect Apify's MCP server (mcp.apify.com) and your AI assistant can pull contact details from a list of sites itself. For example: "extract emails and phone numbers from these 20 company domains and show me which ones are role mailboxes." The only required input is `domains`.

### How much does it cost

Pay per event, with **no per-run start fee**: **$0.0016 per domain with contact details** (`domain` event), charged only when a domain's status is `found`, so a domain with at least one email or phone number. Domains with `not_found`, `blocked`, `unreachable` or `invalid` status are free. 1,000 domains with contacts cost about $1.60.

Paid Apify plans get 10% (Silver) to 20% (Gold and up) off. For comparison, the most-used page-based contact scraper on Apify charges $0.002 per scraped page (about $0.01 for a 5-page domain) and one website email scraper $0.02 per website with emails found. `maxDomains` caps the bill, and if you set a maximum spend per run the Actor stops cleanly when it is reached and keeps the rows already delivered (rows still in progress are not delivered or charged).

### Responsible use

Public does not mean free to use. This Actor reads pages that are public and respects `robots.txt`, but what you do with the addresses is your responsibility.

- **No spam.** Do not send unsolicited bulk email. Personal addresses (`type: personal`) are personal data under GDPR, UK GDPR and similar laws. Turn **Include personal emails** off if you only need company mailboxes.
- **EU and UK:** B2B outreach needs a documented legitimate interest (GDPR Art. 6(1)(f)), an easy way to object and an information notice. Some countries require prior consent. **US:** CAN-SPAM requires honest headers, a physical address and a working opt-out. **Canada** (CASL) and others are stricter.
- **Respect opt-outs.** Keep a suppression list, honour unsubscribe requests and "do not contact" notes on the page.
- **Store only what you need**, for as long as you need it, and delete data when asked.
- **Polite by design.** `robots.txt` is obeyed (a `Disallow` for the bot is respected; a Crawl-delay slows it down, up to 10 s; 5xx on robots.txt means the site is skipped), requests to one site are spaced out, only a few pages are read, and bot-check pages are not bypassed.
- **Safe by design.** The Actor refuses localhost, private networks, link-local and cloud-metadata addresses, also through DNS answers and redirects, and never follows a redirect to another domain. With a proxy, every URL and redirect is still checked, but the connection is not pinned to the checked IP address.
- Free-mail, social-network and large-platform domains (gmail.com, facebook.com, linkedin.com, github.com, google.com, amazon.com, wikipedia.org and similar) are skipped, so it does not harvest private mailboxes or profile pages.

### Honest limits

- **Static HTML only.** Contact details that appear only after JavaScript runs (single-page apps, widgets, contact forms that load addresses on click) are not seen. Those domains come back as `not_found` with a note when the homepage looks script-rendered.
- **Bot protection wins.** Sites behind Cloudflare challenges, CAPTCHAs or aggressive WAFs return `blocked`. The Actor does not try to get around them.
- **Not verified.** Emails are what the site publishes. Nothing is sent and no mail server is contacted, so deliverability is unknown. Run your own verification before you send.
- **No pattern guessing.** If a site publishes no address you get no address. Use the Work Email Finder for pattern-based likely addresses of named people.
- **Hidden addresses:** simple obfuscation is decoded. Emails built by JavaScript, images of addresses, PDFs and contact forms are not.
- **Role vs personal is a name check**, not a verdict about who reads the mailbox. Mailing-list addresses can show as `personal`.
- **Phone numbers:** only numbers that are valid for their country are returned; a national-format number in an unusual region needs the **Default phone country** set.
- **Sites differ.** A few pages per domain is a sample, not a full crawl: raise `pagesPerDomain` (up to 15) for large sites. Subdomains you enter are read as given (`eu.acme.com` is not shortened to `acme.com`).

### FAQ

#### How do I extract emails from a list of domains?

Paste the domains into **Domains or website URLs** and run. You get one row per domain with its emails (`role` or `personal`), phone numbers, contact page and social links. Rows are in input order.

#### Does it find emails that are hidden or obfuscated?

Simple forms, yes: `name [at] site [dot] com`, `(at)`, HTML entities, full-width `＠`, percent-encoded `mailto:` links and Cloudflare-protected addresses. Addresses assembled by JavaScript or shown as images are not found.

#### What if a site has no email?

The row has status `not_found` (site read, nothing published), `blocked` or `unreachable`, with a reason. These rows are free.

#### Which domains are charged?

Only domains with status `found`: at least one email or phone number after your settings (role / personal / own-domain filters, phones on or off). Everything else is free, including invalid inputs and duplicates.

#### Can it find personal Gmail or Outlook addresses of a company's staff?

It does not look up people. It returns addresses a site publishes, including a gmail.com address a small business prints on its contact page (marked `onDomain: false`; set **Only addresses on the site's own domain** to drop them). Free-mail domains in your input list are skipped.

#### Does it respect robots.txt?

Yes. See [Responsible use](#responsible-use). A site that disallows the bot, or whose `robots.txt` returns a server error, is not read and gets status `blocked`.

#### How fast is it?

Domains are read in parallel (10 by default, up to 50), each one request at a time with a 1 second pause. A 5-page domain takes roughly 5 to 10 seconds, so 1,000 domains take about 10 to 20 minutes at the default settings.

#### Is this legal to use?

Reading public pages is generally allowed, but using the addresses for marketing is regulated. See [Responsible use](#responsible-use). This is not legal advice.

### Related tools

- **Work Email Finder: Find Email by Name & Domain (No SMTP)**: pattern-based likely work emails for named people.
- **Phone Number Validation & Lookup: Format, Carrier, Line Type**: check the phone numbers you extract.
- **Company Enrichment API: Domain to Tech, Jobs & Socials**: add tech stack, jobs and social profiles to the same domains.
- **Google Sheets Import & Export: Apify Data, No Login**: send the results to a Google Sheet.

# Actor input Schema

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

The websites to read, one per line: domains or URLs, which are cut back to the site root. Example: acme.com, https://www.example.org/contact. Free-mail (gmail.com), social-network and internal addresses are skipped with a reason, and every input still gets a result row.

## `maxDomains` (type: `integer`):

Hard cap on how many different domains are read. The rest get a free row with status invalid and a reason, and are not read. Also caps your cost.

## `pagesPerDomain` (type: `integer`):

Pages requested per domain: the homepage plus contact, about, team, careers or imprint pages found through its links and common paths such as /contact. robots.txt requests are not counted.

## `extractPhones` (type: `boolean`):

Also return phone numbers from tel: links and page text, normalised to E.164 (+14155552671). Only valid numbers are returned.

## `defaultPhoneCountry` (type: `string`):

Two-letter country code (US, GB, DE...) used to read phone numbers written without +country code. Empty: guessed from the domain's country ending (.de, .co.uk), otherwise US.

## `includeRoleEmails` (type: `boolean`):

Return shared mailboxes such as info@, sales@, support@, hello@, press@ and careers@ (type role).

## `includePersonalEmails` (type: `boolean`):

Return addresses that look like a person's mailbox, such as jane.doe@ (type personal). These are personal data under GDPR and similar laws: see Responsible use in the README before you contact anyone.

## `onlyOwnDomainEmails` (type: `boolean`):

Drop addresses on other domains, such as a small shop's gmail.com address or a partner's address. Every email carries onDomain either way, so you can also filter afterwards.

## `decodeObfuscated` (type: `boolean`):

Decode simple obfuscation: name \[at] site \[dot] com, (at)/(dot), HTML entities (@), full-width @ and Cloudflare email protection (data-cfemail). Plain text and mailto: links are always read.

## `concurrency` (type: `integer`):

How many different domains are read at the same time. One domain is always read one request at a time.

## `delaySecs` (type: `integer`):

Pause between two requests to the same website. A longer Crawl-delay in the site's robots.txt (up to 10 s) wins.

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

Time allowed for one page request, redirects included. Slow or unreachable sites get status unreachable.

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

Optional. Usually not needed: requests come from the Apify platform. A proxy can help with sites that block data-center IPs, but it does not override robots.txt or bot protection.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "python.org",
    "example.com"
  ],
  "maxDomains": 1000,
  "pagesPerDomain": 5,
  "extractPhones": true,
  "includeRoleEmails": true,
  "includePersonalEmails": true,
  "onlyOwnDomainEmails": false,
  "decodeObfuscated": true,
  "concurrency": 10,
  "delaySecs": 1,
  "timeoutSecs": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One item per input domain: status, emails, phone numbers, contact page and social links (all result items of this run, as JSON, CSV, Excel and more).

## `records` (type: `string`):

Key-value store records: the run summary (SUMMARY: counts by status, charged domains, spending-limit flag).

# 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": [
        "apify.com",
        "python.org",
        "example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("data-done-right/domain-email-extractor").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": [
        "apify.com",
        "python.org",
        "example.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("data-done-right/domain-email-extractor").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": [
    "apify.com",
    "python.org",
    "example.com"
  ]
}' |
apify call data-done-right/domain-email-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,data-done-right/domain-email-extractor"
        }
    }
}
```

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/tEdAeOdlxDzqSJjbT/builds/OcZAbk16INtZkWkXC/openapi.json
