# Email Finder — Website Emails, Phones & Socials — $2/1k (`chimerical_quicklime/website-email-finder`) Actor

Email finder for company websites: crawls contact, about, team and imprint pages for emails, phones, socials and addresses. Bulk domains from CSV or Google Sheets, deobfuscation, DNS MX check, empty domains not charged. No login or cookies. MCP-ready for AI agents. $2 per 1,000 records.

- **URL**: https://apify.com/chimerical_quicklime/website-email-finder.md
- **Developed by:** [Khrystyna Skotte](https://apify.com/chimerical_quicklime) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 contact rows

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?

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

## Email Finder — Website Emails, Phones & Socials

Give it a list of company websites and get back the email addresses, phone numbers, social profiles and postal addresses those sites publish, with the page each contact came from. It is an **email finder for domains you already have** — from Google Maps exports, directories, CRMs or a spreadsheet column — not a people database. No login, no cookies, no browser: plain HTTP, 512 MB of memory, about 5–10 seconds per website.

What makes it different from the other email finders in the Store:

- **You are not charged for domains that return nothing.** A website with no email, phone or social profile is listed in the run's OUTPUT summary with the reason (`blocked`, `timeout`, `no_contacts`…) instead of producing a paid empty row.
- **Every email is auditable.** Each address carries the `sourceUrl` it was found on and a `context` snippet of the surrounding text, so you can see *why* it was extracted.
- **Pages are crawled in the order that matters:** home page → contact → imprint/legal notice → about/team/people → support → press → careers → privacy, plus footer links and `sitemap.xml` hints. Ten pages per site is usually enough to find everything.
- **Obfuscated addresses are decoded:** `name [at] domain [dot] com`, HTML entities, Cloudflare email protection (`__cf_email__`), and `mailto:` links with subjects.
- **Deduplicated, lowercased, typed.** Every email is tagged `generic` (info@, sales@, press@…) or `personal` (jason@, first.last@) so you can route role inboxes and named people differently.
- **DNS MX check** (`mxValid`) tells you whether the email's domain can receive mail at all.

### What you get

Per website (`outputMode: perDomain`, the default):

| Field | Example | Notes |
|---|---|---|
| `domain` | `fsf.org` | Registered domain of the website |
| `websiteUrl` | `https://fsf.org/` | The URL you supplied, normalised |
| `websiteTitle` | `Front Page — Free Software Foundation — …` | `<title>` of the home page |
| `redirectedTo` | `null` | Set when the home page redirected to another domain |
| `emails[]` | `{ "email": "info@fsf.org", "type": "generic", "isOffDomain": false, "sourceUrl": "https://www.fsf.org/about/contact/", "context": "…please email info@fsf.org. Thank you…", "name": null, "title": null, "mxValid": true }` | One object per unique email; `isOffDomain` is `true` when the address is on another domain (gmail, agency, partner) |
| `emailCount` | `8` | |
| `primaryEmail` | `campaigns@fsf.org` | First generic inbox, else the first email found |
| `phones[]` | `["+16175425942"]` | From `tel:` links, JSON-LD and page text; duplicates in national and international form are merged |
| `socials{}` | `{ "linkedin": "https://www.linkedin.com/company/creative-commons", "github": "…" }` | facebook, instagram, linkedin, twitter, youtube, tiktok, github, pinterest, threads, telegram, whatsapp |
| `addresses[]` | `["31 Milk Street # 960789, Boston, MA, 02196, US"]` | JSON-LD `PostalAddress` and `<address>` blocks |
| `people[]` | `[{ "name": "…", "title": "…", "email": "…" }]` | From JSON-LD `Person` markup (only when `includeNames` is on) |
| `mxValid` | `true` | MX record exists for the website's own domain |
| `pagesCrawled` | `10` | |
| `status` | `ok` | `ok`, `partial_timeout`, `no_contacts` (only with `includeEmptyDomains`) |
| `scrapedAt` | `2026-10-21T16:16:05.950Z` | |

Per email (`outputMode: perEmail`) you get one flat row per unique address: `email`, `type`, `isOffDomain`, `domain`, `websiteUrl`, `websiteTitle`, `name`, `title`, `sourceUrl`, `context`, `mxValid`, plus the site's `phones`, `socials`, `addresses`, `pagesCrawled`, `status`, `scrapedAt`. This is the shape to import straight into a CRM or an outreach tool.

### What people use it for

1. **Enrich a Google Maps or directory export.** Scrape businesses with any Google Maps actor, feed the website column in here, get emails for the ones that publish them.
2. **Cold outreach lists.** Run a CSV of target domains, filter `type = personal` and `mxValid = true`, and you have named contacts with the page that proves the address is public.
3. **Press and partnership lists.** `emailFilter: generic` returns only press@, partnerships@, info@ inboxes across hundreds of sites in one run.
4. **CRM hygiene.** Re-run your account list monthly; compare `emails[]` with what is in the CRM to catch changed or removed addresses.
5. **Agency prospecting.** Pull socials and phones together with the email to see which channels a business is active on before you reach out.
6. **Compliance and research.** The imprint/legal-notice priority finds the legally required contact details on EU websites (Impressum, mentions légales, aviso legal).

### Input

All fields are optional. With no input at all the actor runs a sample on ten public organisation websites so you can see the output shape.

#### One row per domain (default)

```json
{
    "startUrls": [{ "url": "https://www.fsf.org" }, { "url": "creativecommons.org" }],
    "maxPagesPerDomain": 10,
    "maxDepth": 2,
    "includePhones": true,
    "includeSocials": true,
    "includeNames": true,
    "verifyMx": true,
    "maxItems": 100
}
```

#### One row per email, named people only, same-domain only

```json
{
    "domains": ["apify.com", "basecamp.com", "letsencrypt.org"],
    "outputMode": "perEmail",
    "emailFilter": "personal",
    "onlySameDomain": true,
    "maxItems": 500
}
```

#### Bulk list from a CSV or Google Sheet

```json
{
    "listUrl": "https://docs.google.com/spreadsheets/d/1AbC…/edit#gid=0",
    "outputMode": "perDomain",
    "maxItems": 2000,
    "maxConcurrency": 10
}
```

The first column of the sheet or CSV is read (a header such as `domain`, `website` or `url` is skipped). Share the sheet as "anyone with the link". You can combine `startUrls`, `domains` and `listUrl` in one run; duplicates are removed.

#### All inputs

| Field | Default | What it does |
|---|---|---|
| `startUrls` | — | Websites as `{ "url": … }` objects (the Apify URL list editor) |
| `domains` | — | Websites as plain strings; `urls` is accepted as an alias |
| `listUrl` | — | CSV, TXT, Google Sheet or Google Drive file with one website per line |
| `outputMode` | `perDomain` | `perDomain` or `perEmail` |
| `maxItems` | `10` | Rows to stop at (domains or emails depending on the mode) |
| `maxPagesPerDomain` | `10` | Pages fetched per site (`maxPagesPerSite` and `maxRequestsPerStartUrl` are accepted as aliases) |
| `maxDepth` | `2` | Link depth from the home page; `0` = home page only |
| `emailFilter` | `all` | `all`, `generic` (role inboxes) or `personal` (named people) |
| `onlySameDomain` | `false` | Drop emails on other domains (gmail, agencies, partners) |
| `verifyMx` | `true` | DNS MX lookup per email domain → `mxValid` |
| `deobfuscate` | `true` | Decode `[at]`/`[dot]`, entities, Cloudflare protection |
| `includePhones` / `includeSocials` / `includeNames` | `true` | Toggle the extra signals |
| `includeEmptyDomains` | `false` | Also save (and pay for) rows for domains with no contacts |
| `perDomainTimeoutSecs` | `60` | A slow site stops after this; what was found is kept |
| `maxConcurrency` | `5` | Websites crawled in parallel |
| `proxyConfiguration` | off | Apify proxy for sites that block datacenter IPs (`status: blocked`) |

### Output example

A real `perDomain` row from a run on 2026-10-21 (trimmed to three emails):

```json
{
  "domain": "fsf.org",
  "websiteUrl": "https://fsf.org/",
  "websiteTitle": "Front Page — Free Software Foundation — working together for free software",
  "redirectedTo": null,
  "emails": [
    { "email": "campaigns@fsf.org", "type": "generic", "isOffDomain": false, "name": null, "title": null, "sourceUrl": "https://www.fsf.org/", "context": "Send your feedback on our translations and new translations of pages to campaigns@fsf.org.", "mxValid": true },
    { "email": "info@fsf.org", "type": "generic", "isOffDomain": false, "name": null, "title": null, "sourceUrl": "https://www.fsf.org/about/contact/", "context": "olunteer opportunities, shop items, meeting staff, etc., please email info@fsf.org. Thank you for your interest", "mxValid": true },
    { "email": "membership@fsf.org", "type": "generic", "isOffDomain": false, "name": null, "title": null, "sourceUrl": "https://www.fsf.org/associate/contact.html", "context": "You can email us with any membership questions or support requests: membership@fsf.org or contact the Free Software Foundation.", "mxValid": true }
  ],
  "emailCount": 8,
  "primaryEmail": "campaigns@fsf.org",
  "phones": ["+16175425942"],
  "socials": {},
  "addresses": ["31 Milk Street # 960789, Boston, MA, 02196, US"],
  "people": [],
  "mxValid": true,
  "pagesCrawled": 10,
  "status": "ok",
  "error": null,
  "scrapedAt": "2026-10-21T16:16:05.950Z"
}
```

And a `perEmail` row from the same day:

```json
{
  "email": "press@letsencrypt.org",
  "type": "generic",
  "isOffDomain": false,
  "domain": "letsencrypt.org",
  "websiteUrl": "https://letsencrypt.org/",
  "websiteTitle": "Let's Encrypt",
  "name": null,
  "title": null,
  "sourceUrl": "https://letsencrypt.org/contact/",
  "context": "Email: press@letsencrypt.org",
  "mxValid": true,
  "phones": [],
  "socials": { "github": "https://github.com/letsencrypt", "linkedin": "https://www.linkedin.com/company/lets-encrypt" },
  "addresses": [],
  "pagesCrawled": 10,
  "status": "ok",
  "scrapedAt": "2026-10-21T16:17:08.754Z"
}
```

The run's **OUTPUT** record (key-value store) summarises everything: rows saved, emails found, domains requested, and a `failed[]` list with a reason for every domain that produced no row.

### Pricing

Pay per event, no subscription:

- **$0.001 per run start** (per GB of memory; the actor runs at 512 MB, so one start event).
- **$0.002 per row saved** — one website in `perDomain` mode (with *all* of its emails, phones and socials), or one email in `perEmail` mode.

So **1,000 websites cost about $2** in `perDomain` mode plus Apify platform usage (roughly $0.2 per 1,000 sites at 512 MB — the test run above processed 10 sites for $0.0017 all-in). Domains that return no contacts are not charged. For comparison, the per-page email extractors in the Store charge $0.001–0.002 per page *crawled* (so $0.01–0.02 per 10-page site), and the per-row ones $0.02 per site; the big "leads finder" databases charge $0.003 per lead and bill you for leads without emails.

### Good to know

- **No SMTP verification, by design.** Apify blocks outbound port 25, so no actor running on the platform can really talk to a mail server. `mxValid` is an honest DNS check: `true` means the domain has mail exchangers, `false` means mail to that domain will bounce, `null` means DNS did not answer. Treat "verified email" claims from other cloud actors accordingly.
- **Sites rendered only by JavaScript** (and sites behind aggressive bot protection) may return `status: blocked` or `no_contacts`. Turn on `proxyConfiguration` with Apify residential proxy for the blocked ones; a browser-based crawler is the fallback for pure single-page apps.
- **Email coverage depends on the website.** Many small businesses only publish a contact form. Those domains show up in the OUTPUT summary as `no contacts found` and are not charged — the dataset only contains sites that actually publish something.
- **Junk is filtered.** Addresses on third-party infrastructure (mail-archive.com, sentry.io, wixpress.com, schema.org, googlegroups.com, CDNs, platforms…), `noreply@`/`donotreply@` senders, image names like `logo@2x.png`, hash-like local parts and other sites' mailing lists (`lists.*`) are dropped. Off-domain addresses that survive are kept but flagged `isOffDomain: true`; set `onlySameDomain: true` to drop them too.
- **`type` is a heuristic.** Known role words, hyphenated roles (`license-violation@`), plural or `-ing`/`-tion` nouns and list servers are `generic`; everything else is `personal`. Filter on it, but spot-check.
- **Names and titles** are filled when a person's name sits next to their email on a page (staff or team pages, author boxes) or in JSON-LD `Person` markup. On fsf.org's staff page, for example, 12 of 13 emails came back with name and title (`zoe@fsf.org` → `Zoë Kooyman`, `Executive Director`). Contact pages with only role inboxes return `null`.
- **Politeness:** one site at a time is crawled with a short random delay between pages, up to `maxPagesPerDomain` pages, inside `perDomainTimeoutSecs`. The crawl never leaves the website's own domain.
- **Legal:** the actor only collects contact details a company publishes on its own website. You are responsible for using them in line with GDPR, CAN-SPAM and the laws that apply to your outreach.

### Integrations

- **API:** `POST https://api.apify.com/v2/acts/chimerical_quicklime~website-email-finder/run-sync-get-dataset-items?token=…` with the JSON input above returns the rows directly; for big lists start a run with `/runs` and read the dataset when it finishes.
- **Python / JavaScript:** `apify-client` — `client.actor("chimerical_quicklime/website-email-finder").call(run_input={...})` then `client.dataset(run["defaultDatasetId"]).list_items()`.
- **Schedules + webhooks:** schedule the actor on your account list and add a webhook on `ACTOR.RUN.SUCCEEDED` to push new rows into your CRM.
- **Make, Zapier, n8n:** use the Apify module/node, choose this actor, map `email`, `domain`, `sourceUrl` and `mxValid` to your sheet or CRM fields. `perEmail` mode gives one record per row, which is what these tools expect.
- **MCP / AI agents:** add the actor to your Apify MCP server; an agent can call it with a list of domains and read the `OUTPUT` summary plus the dataset.
- **Export:** JSON, CSV, Excel, XML and HTML from the dataset tab or the API.

### FAQ

**Can it find an email for a specific person?** No. It finds the emails a website publishes. For "first name + last name + domain" guessing use a pattern finder; for named decision-makers use a people database.

**Why did a domain not appear in the dataset?** It produced no email, phone or social within `maxPagesPerDomain` pages and `perDomainTimeoutSecs`, or it was blocked. The reason is in the OUTPUT record. Set `includeEmptyDomains: true` if you want a row for it anyway.

**Does it crawl the whole site?** No — only up to `maxPagesPerDomain` pages, chosen by relevance (contact and about pages first), never off-domain.

**Can I pass full URLs instead of domains?** Yes. A deep URL is crawled from that page; links on it are followed within the same domain.

**How fast is it?** About 5–10 seconds per website at the defaults; `maxConcurrency` crawls several websites at once. 1,000 websites take roughly 20–30 minutes at `maxConcurrency: 10`.

# Actor input Schema

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

Company websites to scan, one per line (https://example.com or just example.com). Leave everything empty to run a sample on a few public organisation sites.

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

Same as Website URLs, as bare domains (acme.com). Handy when pasting from a spreadsheet column.

## `listUrl` (type: `string`):

URL of a CSV, TXT or Google Sheet (shared as 'anyone with the link'). The first column is read; a header row like 'domain' or 'website' is skipped.

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

perDomain: one row per website with all its emails, phones, socials and addresses. perEmail: one row per email address (flat, CRM-ready).

## `maxItems` (type: `integer`):

Stop after this many rows (domains in perDomain mode, emails in perEmail mode).

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

Pages fetched per website. The home page is always first, then contact, imprint, about/team, support, press, careers, privacy pages and footer links, plus sitemap.xml hints.

## `maxDepth` (type: `integer`):

How many clicks away from the home page to follow links (0 = home page only).

## `emailFilter` (type: `string`):

all: every email. generic: role inboxes only (info@, contact@, sales@…). personal: named people only (john.smith@…).

## `onlySameDomain` (type: `boolean`):

Drop emails whose domain differs from the website (agency, partner or gmail addresses).

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

Look up the DNS MX record of each email's domain and report mxValid true/false. This proves the domain can receive mail; it is not an SMTP mailbox check (Apify blocks port 25, so no actor can do that honestly).

## `deobfuscate` (type: `boolean`):

Recover 'name \[at] domain \[dot] com', HTML-entity, Cloudflare email-protection and mailto-with-subject forms.

## `includePhones` (type: `boolean`):

Phone numbers from tel: links, JSON-LD and page text (ZIP codes, years and prices are filtered out).

## `includeSocials` (type: `boolean`):

Facebook, Instagram, LinkedIn, X/Twitter, YouTube, TikTok, GitHub, Pinterest, Threads, Telegram, WhatsApp.

## `includeNames` (type: `boolean`):

Names/titles next to emails on team pages and from JSON-LD Person/Organization markup.

## `includeEmptyDomains` (type: `boolean`):

By default a domain with no email, phone or social is only listed in the run OUTPUT (and not charged). Turn on to get a row for it anyway.

## `perDomainTimeoutSecs` (type: `integer`):

A slow site stops after this many seconds; whatever was found by then is kept.

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

How many websites are crawled at the same time.

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

Off by default. Use Apify residential proxy for sites that block datacenter IPs (status 'blocked' in the OUTPUT summary).

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.eff.org"
    },
    {
      "url": "https://www.gnu.org"
    }
  ],
  "outputMode": "perDomain",
  "maxItems": 10,
  "maxPagesPerDomain": 10,
  "maxDepth": 2,
  "emailFilter": "all",
  "onlySameDomain": false,
  "verifyMx": true,
  "deobfuscate": true,
  "includePhones": true,
  "includeSocials": true,
  "includeNames": true,
  "includeEmptyDomains": false,
  "perDomainTimeoutSecs": 60,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Dataset of website contacts (one row per domain or per email, JSON).

# 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 = {
    "startUrls": [
        {
            "url": "https://www.eff.org"
        },
        {
            "url": "https://www.gnu.org"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("chimerical_quicklime/website-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 = { "startUrls": [
        { "url": "https://www.eff.org" },
        { "url": "https://www.gnu.org" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("chimerical_quicklime/website-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 '{
  "startUrls": [
    {
      "url": "https://www.eff.org"
    },
    {
      "url": "https://www.gnu.org"
    }
  ]
}' |
apify call chimerical_quicklime/website-email-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,chimerical_quicklime/website-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/0Yn562Bc7uWqifCPT/builds/agl2rJCVDfWoRull3/openapi.json
