# Website Email Finder — Public Sources (`meka.im/website-email-finder`) Actor

Website email finder and domain email scraper for public company contact pages. Extract published email addresses with the exact source URL and surrounding text. No guessing or login. Pay only when at least one address is found.

- **URL**: https://apify.com/meka.im/website-email-finder.md
- **Developed by:** [Meka.im](https://apify.com/meka.im) (community)
- **Categories:** Lead generation, Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 contact sets

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

Use this **website email finder** to extract public company email addresses from a list of domains. It returns the addresses that **actually appear on
those public pages**, and for every single address it tells you **which URL it came from,
which page type it was, and the surrounding text**, so you can open the page and check.

**$4 per 1,000 domains where we find at least one address.** Domains where we find nothing,
domains that block us, and domains that no longer resolve are all written to the dataset
so you can see what happened — and none of them are charged.

No login, no cookies, no browser, no proxy pool of your own. Only public pages.

### What it does not do

This matters more than the feature list, because most tools in this category quietly do one
of these and call the result an "email":

- **It never guesses an address.** No `firstname.lastname@domain`, no `info@domain` fallback,
  no pattern permutations. If the address is not written on a page, you do not get it.
  Passing a person's name as input is rejected with an error rather than silently ignored.
- **It does not verify mailboxes.** There is an `mxAvailable` field, which says whether the
  email's *domain* publishes MX records. That is a DNS fact about the domain, not a check of
  the individual mailbox, and not a deliverability guarantee.
- **It does not look anyone up in a database.** Everything comes from the pages themselves.
- **It does not return employees' personal addresses.** What you get is whatever the company
  chose to publish on its own website.

### Input: find emails from website domains

```json
{
  "domains": ["apify.com", "acme-widgets.de", "example-shop.co.uk"],
  "maxPages": 5
}
```

Every domain produces one row:

```json
{
  "domain": "acme-widgets.de",
  "websiteUrl": "https://acme-widgets.de/",
  "status": "contacts_found",
  "emailCount": 3,
  "sameDomainCount": 2,
  "freeMailCount": 1,
  "emails": [
    {
      "email": "info@acme-widgets.de",
      "sourceUrl": "https://acme-widgets.de/kontakt",
      "sourcePage": "contact",
      "sourceType": "entity",
      "discoveryMethod": "html-entity-decode",
      "context": "… Haben Sie eine Frage? Schreiben Sie an info@acme-widgets.de oder rufen Sie …",
      "emailDomain": "acme-widgets.de",
      "isSameDomain": true,
      "isFreeMailProvider": false,
      "addressType": "role",
      "mxAvailable": true,
      "firstSeenAt": "2026-09-21T09:14:02.118Z"
    }
  ],
  "pagesScraped": [
    { "url": "https://acme-widgets.de/", "page": "home", "outcome": "ok", "status": 200 },
    { "url": "https://acme-widgets.de/kontakt", "page": "contact", "outcome": "ok", "status": 200 }
  ]
}
```

### Quick start: how the website email scraper works

The homepage, plus up to four more chosen deterministically from the homepage's own links:
**contact, about, team, legal/imprint** — one of each, same registrable domain only.
It is not a site crawler; five pages is the ceiling, and `maxPages` lowers it.

Link text and paths are matched in many languages, so `/kontakt`, `/contacto`, `/contatti`,
`/iletisim`, `/お問い合わせ` and `/연락처` are all recognised.

### Output: how each address is found

Seven sources, in order of how strong the evidence is. The share of addresses each one
contributed in our own 1,039-domain test is in brackets:

| Source | What it is | Share |
|---|---|---|
| `mailto` | `<a href="mailto:…">` | 60.7% |
| `entity` | HTML-entity encoded, e.g. `info&#64;example.com` | 18.9% |
| `cfemail` | Cloudflare's `data-cfemail` obfuscation, decoded with its public scheme | 11.1% |
| `jsonld` | schema.org `email` field | 5.9% |
| `text` | plain text on the page | 1.4% |
| `script` | a structured `"email":` field in page data | 1.2% |
| `obfuscated` | clearly marked forms such as `info [at] example [dot] com` | 0.8% |

Addresses written as `X at Y dot Z` are only restored when the `dot` marker is there too.
Text like "learn more at Visa.com" is left alone — restoring that would be guessing.

### What the labels mean

| Field | Meaning |
|---|---|
| `isSameDomain` | The address is on the input domain (subdomains count) |
| `isFreeMailProvider` | gmail, outlook, gmx and similar. Kept, because small businesses often publish one as their contact address, but counted separately |
| `isDisposableDomain` | The domain is on a list of known throwaway-mail providers. The list is not exhaustive |
| `addressType` | `role` (info@, sales@, kontakt@ …), `personal`, or `unknown` |
| `mxAvailable` | Whether the email domain publishes MX records. A DNS fact, not a mailbox check |

Addresses belonging to website builders, analytics and consent tools (Wix, Squarespace,
Sentry, Cookiebot and about 45 others) are **excluded by default** — they are the template's
address, not the company's. The count is reported as `excludedVendorCount` rather than hidden.

### Pricing

| | |
|---|---|
| Price | **$4.00 per 1,000 delivered contact sets** (`$0.004` each) |
| What counts as one | One domain where at least one address was found |
| Start fee | None |
| Charged for a domain with 1 address | Once |
| Charged for a domain with 9 addresses | Once |
| Charged for a domain where nothing was found | **Never** |
| Charged for a blocked, timed-out or dead domain | **Never** |
| Charged for the same domain twice in one list | **Never** — duplicates are removed before crawling |

Set **Maximum charge** on the run to cap spend. The run stops before it would exceed it.

### What to expect

From a 1,039-domain cold test across 50 countries
(489 Google Maps business sites, 307 LinkedIn company sites, 243 creator sites):

| | Reachable | At least one address on the site's own domain | Any public address |
|---|---|---|---|
| Local business websites | 90.6% | 59.8% | **77.4%** |
| Company websites | 80.5% | 60.9% | **69.0%** |
| Creator / small sites | 90.1% | 32.9% | 46.1% |

Reading the four pages beyond the homepage found addresses on **13.8% more domains** than
the homepage alone.

Results depend on the sites you feed it. A domain that publishes no address anywhere returns
no address — and costs nothing.

### Ordering, failures and resuming

- Dataset order is your input order, after duplicates are removed. A domain that fails keeps
  its position instead of letting later domains jump ahead.
- If a run is aborted and resumed, already-delivered domains are recognised from the dataset
  and are neither re-delivered nor re-charged.
- Failures are reported per domain in `outcome`: `http_403`, `http_429`, `dns_failed`,
  `timeout`, `tls_failed`, `challenged`, `http_5xx` and so on — each is its own bucket, so
  "the site blocked us" never looks like "the site has no address".

### Limits

- Five pages per domain. A contact address buried on page six is not found.
- No JavaScript is executed. An address that only exists after a script runs is not found.
- Pages behind a login, a paywall or a CAPTCHA are not opened. They are reported as
  `challenged` and not charged.
- Images are not read. An address published only as a picture is not found.
- The disposable-domain and vendor-domain lists are curated, not exhaustive.

### Privacy

This Actor reads public web pages. It stores nothing about the pages beyond what is in your
dataset, and it sends nothing to third parties. The addresses it returns were published by
the site owners themselves on their own public pages.

Use of the data is your responsibility: contacting people has rules — GDPR, CAN-SPAM, CASL
and similar laws — and publishing an address is not the same as consenting to be emailed.

### Legal

Not affiliated with, endorsed by, or connected to any of the websites it reads.

### API and automation

Run it from the Apify API, a scheduled task, or as a step in a workflow. The input is the
same JSON shown above; results land in the run's default dataset.

```bash
curl -X POST "https://api.apify.com/v2/acts/<actor-id>/runs?token=<token>&maxTotalChargeUsd=2" \
  -H "content-type: application/json" \
  -d '{"domains":["apify.com","fietstop.com"],"maxPages":5}'
```

Pair it with a Google Maps or LinkedIn company scraper: take the `website` field from those
results, feed it in as `domains`, and you get the contact set for each one. Because duplicate
domains are removed before crawling, the same company appearing twice in your list is only
crawled — and only charged — once.

Related discovery tools:

- [Google Maps Business Scraper](https://apify.com/meka.im/google-maps-business-scraper) finds local businesses, websites, phones, addresses, and opening hours.
- [Google Maps Reviews Scraper](https://apify.com/meka.im/google-maps-reviews-scraper) adds customer-review and reputation signals.
- [LinkedIn Company Scraper](https://apify.com/meka.im/linkedin-company-scraper) returns public company details and website domains.

### FAQ and troubleshooting

**Why does a domain I know has an address come back empty?**
Five pages per domain is the ceiling and JavaScript is not executed. An address on a sixth
page, inside an image, or injected by a script after load is not found. That domain costs
nothing.

**Why is `status` `unreachable`?**
Check `outcome` on the same row: `http_403` means the site refused us, `dns_failed` means the
domain no longer resolves, `challenged` means a CAPTCHA or bot-check page was served. Each is
its own bucket precisely so that "blocked" never gets mistaken for "no address published".

**An address has `isSameDomain: false`. Is it wrong?**
Not necessarily. Many small businesses publish a gmail or outlook address as their contact.
Those are marked with `isFreeMailProvider: true` so you can decide. Addresses belonging to
website builders and tooling vendors are removed before you see them.

**What does `mxAvailable: true` tell me?**
Only that the email's domain publishes MX records. It says nothing about whether that
particular mailbox exists or accepts mail.

**Can I give it a person's name and get their work address?**
No. Passing a name field makes the run fail with an explanation rather than returning a
constructed address.

# Actor input Schema

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

One domain per line, for example acme-widgets.de. A full URL works too; www. is stripped and duplicates are removed.

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

Website URLs to start from, when you want a specific page instead of the homepage. Combined with the domains above.

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

Hard cap on how many domains are written to the dataset across the whole run, including a resumed run.

## `maxPages` (type: `integer`):

Homepage plus up to four more: contact, about, team and legal/imprint. Lower it to 1 to scan homepages only — that is faster and cheaper, and in our own 1,039-domain test it found addresses on 39.9% of domains instead of 53.7%.

## `includeFreeMail` (type: `boolean`):

Keep gmail.com, outlook.com and similar addresses when they are published as the site's contact address. Small businesses often use one. They are always labelled separately from addresses on the site's own domain.

## `checkMx` (type: `boolean`):

Adds mxAvailable to each address: whether the email domain publishes MX records. This is a DNS fact about the domain, not a check of the individual mailbox and not a deliverability guarantee.

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

How many batches of 8 domains run at once. Raise it for large lists, lower it if you want a gentler crawl.

## Actor input object example

```json
{
  "domains": [
    "apify.com"
  ],
  "maxResults": 1000,
  "maxPages": 5,
  "includeFreeMail": true,
  "checkMx": true,
  "concurrency": 4
}
```

# Actor output Schema

## `contactSets` (type: `string`):

No description

## `withContacts` (type: `string`):

No description

## `runSummary` (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": [
        "apify.com",
        "acme-widgets.de"
    ],
    "maxResults": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("meka.im/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 = {
    "domains": [
        "apify.com",
        "acme-widgets.de",
    ],
    "maxResults": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("meka.im/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 '{
  "domains": [
    "apify.com",
    "acme-widgets.de"
  ],
  "maxResults": 1000
}' |
apify call meka.im/website-email-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,meka.im/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/Ho9T3h7MqRoJ7F7rq/builds/xTFLF0aOafEATOs3L/openapi.json
