# Website Email & Contact Scraper - Scored Leads (`berkaydev/website-email-contact-scraper`) Actor

Find the e-mail, phone and socials behind any list of websites - plus a technical profile (CMS, SSL, mobile, analytics, site age) and a lead score telling you who is worth contacting first. Pay only for contacts found.

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

## Pricing

from $6.00 / 1,000 contact founds

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/platform/actors/running/actors-in-store#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

**Find the e-mail address, phone number and social profiles behind any list of websites** — and,
in the same run, learn which of those businesses are actually worth contacting. Paste in domains,
get back contactable, scored leads. No API key, no browser, no LLM.

Most contact scrapers stop at the address. This one also reads what the website says about the
business running it — which CMS it uses, whether it has SSL, whether it works on a phone, when it
was last touched, whether anyone is measuring anything — and turns that into two auditable scores.
Those few fields are the difference between an address list and a work list.

On fresh test batches it had never seen — roofing contractors in Essen, Germany and Austin, Texas —
it found an e-mail address for **78%** and **80%** of them respectively. The measurement, and an
honest list of what it gets wrong, are further down.

### What it does

Give it `example.com` (or a full URL — both work) and for every site it will:

1. **Fetch the homepage** and look for contact details in descending order of confidence:
   `mailto:` links, Cloudflare-obfuscated addresses, schema.org structured data, the raw source,
   then the visible text. Addresses written as `name [at] example [dot] com` are decoded.
2. **Follow the contact page** when the homepage came up short — the discovered link plus the
   paths most business sites have anyway (`/kontakt`, `/impressum`, `/contact`, `/contacto`, …).
3. **Profile the site** from HTML it already downloaded — no extra requests, no extra time.
4. **Score the lead** on two separate questions, and show its work.

Give it a **link-in-bio hub** instead — `linktr.ee/name`, Beacons, Stan, Taplink and a couple of
dozen others — and it opens the hub, finds the real website behind it and audits that. This matters
more than it sounds: pointed at a Linktree, a scraper that does not resolve reads the hub's own page
and returns whatever it finds there. On a live test that produced a Linktree advertiser's address as
the contact for a Berlin tattoo studio. Resolved, the same input returns the studio's own address on
its own domain. Businesses whose entire web presence turns out to be a hub come back tagged
`linktree_only` — for anyone selling web work, those are the best rows in the file.

### How the scoring works

Two scores, deliberately not merged. A flawless website that answers every channel is a *poor*
lead for a web agency; a neglected one you can barely reach is a good one. One number would hide
exactly that.

#### `contactabilityScore` — can you reach them?

| Signal | Points |
|---|---|
| E-mail, personal (`jan.meier@…`) | 50 |
| E-mail, role (`info@`, `kontakt@`, `office@`, `hello@`) | 40 |
| E-mail, department (`press@`, `jobs@`, `support@`, `datenschutz@`) | 15 |
| E-mail, `noreply@` | 0 |
| Phone from a `tel:` link | 25 |
| Phone from labelled text ("Tel: …") | 20 |
| Contact form | 15 |
| Any social profile | 10 |

E-mail points are multiplied by how much the source can be trusted: **1.0** for a `mailto:` link,
structured data, a Cloudflare-obfuscated address or a contact page — these were published as
contact points on purpose. **0.75** for a regex hit in the raw source or page text, which could be
anything. The result is reported as `emailConfidence: high | medium`.

Capped at 100.

#### `opportunityScore` — do they visibly need help?

| Finding | Points | |
|---|---|---|
| `site_unreachable` — the website did not respond | 40 | |
| `stale_site_severe` — copyright 6+ years old | 35 | |
| `no_ssl` — still on http | 28 | |
| `not_mobile_friendly` — no viewport meta tag | 25 | |
| `stale_site` — copyright 3–5 years old | 20 | |
| `outdated_cms_hint` — Wix or Squarespace | 12 | |
| `no_analytics` — no GA, Meta Pixel, Hotjar or LinkedIn Insight | 10 | |
| `no_social_presence` — no social links found | 8 | |
| `no_contact_form` — only ever reported if a contact page was actually opened | 7 | |

Capped at 100. Every finding that fired is listed in `opportunityTags`.

#### `leadPriority`

```
contactabilityScore == 0                        → cold   (no way to act on it)
contactability ≥ 50 and opportunity ≥ 25        → hot
contactability ≥ 50 and opportunity ≥ 10        → warm
contactability ≥ 50  or opportunity ≥ 25        → warm
otherwise                                       → cold
```

#### It shows its work

Every row carries a `scoreBreakdown` naming each contribution, so the ranking can be audited
rather than believed:

```json
{
  "website": "https://www.maler-stefanbeckmann.de/",
  "email": "hallo@malermeister-stefanbeckmann.de",
  "emailType": "role",
  "emailConfidence": "high",
  "contactabilityScore": 85,
  "opportunityScore": 57,
  "leadPriority": "hot",
  "opportunityTags": ["stale_site_severe", "outdated_cms_hint", "no_analytics"],
  "scoreBreakdown": {
    "email:role": 40, "phone": 25, "social_profiles": 10,
    "opportunity:stale_site_severe": 35,
    "opportunity:outdated_cms_hint": 12,
    "opportunity:no_analytics": 10
  }
}
```

Read plainly: reachable by e-mail, phone and social, on a Wix site last touched six years ago with
no analytics installed. That is a conversation waiting to happen.

**These weights are reasoned judgements, not learned values**, and they assume you sell websites,
hosting or marketing. If you sell something else, ignore `opportunityScore` and sort by
`contactabilityScore` and `emailType` instead — the raw fields are all there.

### Who it is for

**Agencies and freelancers** building a prospect list: the tags say who needs a new website, who
runs no tracking, and who is invisible on a phone.

**Sales teams** turning a list of company domains into something they can mail or call.

**Anyone holding the output of another scraper.** Google Maps, directories, marketplaces — nearly
every scraper returns websites but no e-mail addresses. This is the step that comes after, and it
takes that output directly.

**AI agents and automations.** One required field, everything else defaulted, a flat JSON row per
website and a machine-readable `RUN_STATUS` record. An empty result always states its reason, so a
failed run can never be mistaken for "this business has no contact details".

### How to use it

1. Paste your websites into **Websites**, one per line. Plain domains are fine.
2. Leave **Also profile the website** on unless you only want raw contacts.
3. Run it, then sort the output by `leadPriority` and start at the top.

Duplicates pointing at the same host are removed, so you are never billed twice for one business.

### Input

| Field | Default | What it does |
|---|---|---|
| `startUrls` | — | Websites to process. Domains, full URLs, or link-in-bio hubs. **Required.** |
| `resolveLinkHubs` | `true` | Open a Linktree, Beacons, Stan or Taplink URL and audit the real website behind it instead of the hub itself |
| `enrichSite` | `true` | Profile the site (CMS, SSL, mobile, age, analytics, socials) |
| `maxContactPages` | `5` | How hard to look when the homepage yields nothing. `0` = homepage only |
| `onlyWithContact` | `false` | Drop rows where nothing was found |
| `maxConcurrency` | `10` | Sites fetched in parallel |
| `maxRunTimeSecs` | `3000` | Stops cleanly before this and keeps everything found |

### Output

One row per website, whether or not something was found. Download as JSON, CSV, Excel or HTML, or
pull it through the API.

| Field | Meaning |
|---|---|
| `inputUrl`, `website` | What you supplied, and what was actually audited. They differ only when the input was a link hub and the real site behind it was found |
| `websiteSource` | `input` or `link_hub` — how the audited site was arrived at |
| `statusNote` | One plain sentence explaining a row that looks empty: a hub with no site behind it, a hub that rate-limited us, a dead domain, or a host that refused us |
| `hub`, `hubUrl`, `hubStatus`, `hubFetchStatus`, `hubLinks` | Which hub was opened, what came of it, and every link it carried |
| `finalUrl` | Where the audited site ended up after redirects |
| `siteReachable`, `fetchStatus` | `ok`, `http_403`, `timeout`, `dns_error`, `connect_error`. Only a genuine non-answer counts as a sales signal — a 403 means the host refused *us*, which says nothing about the business |
| `email`, `emailSource`, `emailType`, `emailConfidence` | Address, where it was found, who is behind it, how much to trust it |
| `phone`, `phoneSource` | Normalised number, from a `tel:` link or a labelled line |
| `contactPageUrl`, `contactPageChecked` | Which contact page was opened, if any |
| `socialLinks` | Facebook, Instagram, LinkedIn, X, TikTok, YouTube |
| `cms` | Detected from structural evidence only, never a brand name mentioned in text |
| `copyrightYear` | Latest year in the footer; ranges like "© 2004–2026" handled correctly |
| `hasSsl`, `mobileFriendly` | From the final URL after redirects, and the viewport meta tag |
| `hasTracking`, `trackingTools` | Google Analytics, Meta Pixel, Hotjar, LinkedIn Insight |
| `hasContactForm` | A form carrying an e-mail field or a textarea |
| `contactabilityScore`, `opportunityScore`, `opportunityTags`, `leadPriority`, `scoreBreakdown` | The scoring above |

The key-value store also holds `RUN_STATUS` with the outcome, how many websites were processed and
the e-mail hit rate for the run.

### Pricing

You pay **per contact found** and **per site profiled**. A website that yields nothing costs
nothing — charging for a miss would be charging for our own failure. Any single run can be capped
from the Apify console.

### What it does well, and where it falls short

Every scraper listing claims to be accurate. Here is what was actually measured, and what is
genuinely wrong with this one — you will find out either way, and it is cheaper for both of us if
you find out before you pay.

#### Measured on samples it had never seen

Both test batches were pulled fresh from Google Maps *after* development finished, so none of them
influenced the code. Nine roofing companies in Essen, Germany, and ten in Austin, Texas:

| | Germany | USA |
|---|---|---|
| E-mail found | **78%** | **80%** |
| Phone found | 78% | 90% |
| Refused entry (`403`) | 0 | 1 |

A sample of records was then checked by hand against the live sites. That check found one real
defect — a phone number mangled by a URL-encoded space in a `tel:` link — which has been fixed and
re-verified. Everything else held, including the cases where "nothing found" turned out to be the
correct answer rather than a miss.

#### What it does better than the alternatives

**It ranks leads, and shows why.** Every other contact scraper hands you addresses. This one tells
you which businesses are worth the call and prints its arithmetic in `scoreBreakdown`, so you can
disagree with it on the evidence rather than on faith.

**It declares what it does not know.** `emailConfidence` separates an address published in a
`mailto:` link from a regex hit in body text. `fetchStatus` distinguishes "their site is down"
from "their host refused us". `likelyJsRendered` marks pages we could not really read. An empty
result always carries a reason in `RUN_STATUS`.

**It costs almost nothing to run** — around two seconds per website, no browser, no proxy required,
comfortably inside the free tier.

#### Where it falls short

**About 5% of hosts refuse it outright.** The Actor already retries with several browser
fingerprints, which cleared two thirds of the refusals seen in testing, but some WAFs check for
JavaScript execution and cannot be talked around. Those rows say `fetchStatus: http_403` and
deliberately carry **no** opportunity signal — the Actor will not tell you a business has a broken
website when the truth is it would not let us in. A residential proxy usually gets through them.

**Sites that build themselves in the browser come back empty.** Being browser-free is what makes
this fast and cheap; the cost is that a single-page app hands us a shell. Those rows are flagged
`likelyJsRendered`, and the "no social presence" and "no contact form" findings are suppressed for
them, because we never actually saw the page.

**Link hubs throttle, and there is no way around it.** Linktree serves roughly ten to eleven
requests per run before answering HTTP 429. Measured: fifteen hub URLs in one run left four
unresolved; a longer backoff changed nothing except the runtime, and routing through Apify's shared
datacenter proxy made it worse, not better. Those rows come back `hubStatus: fetch_failed` with
`hubFetchStatus: http_429`, carry no opportunity signal, and cost you nothing — you are billed for
contacts found and sites profiled, never for a hub we could not open. If your list is unusually
hub-heavy, split it across runs. On ordinary lead lists, where hubs are a small minority of rows,
you will not meet this limit.

**`no_analytics` can still misfire.** Twelve analytics products are detected, including Matomo,
Plausible and Cloudflare Insights. Server-side tracking leaves no trace in the HTML and is
undetectable by anyone, including this Actor.

**The scoring weights are reasoned judgements, not learned values.** They were set by argument, not
by measuring which leads actually converted — nobody has that data yet. They also assume you sell
websites, hosting or marketing. If you sell something else, the opportunity signals mean little to
you; sort by `contactabilityScore` and `emailType` instead.

**Phone extraction is deliberately conservative.** It reads `tel:` links and numbers sitting behind
a "Tel:"-style label, and ignores unlabelled digits on a page. That loses a few real numbers rather
than inventing wrong ones, because you are billed per contact found.

**Results are a snapshot.** A host that times out today may answer tomorrow. And the verification
above rests on two batches of around ten sites, with a handful checked by hand — enough to find the
error classes, not enough to put a confidence interval on the hit rate.

**It is new.** No usage history, no ratings, no track record. Judge it on the run, not on the
listing.

### Legal note and support

The Actor reads publicly available pages, the way a browser does. Business contact details
published in an imprint or on a contact page are public information, but what you may do with them
afterwards is regulated — under GDPR in the EU, and in Germany unsolicited B2B e-mail advertising
generally requires prior consent under the UWG. Please make sure your intended use is lawful where
you operate.

Found a bug, or need another signal in the output? Open an issue on the Issues tab.

# Actor input Schema

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

The websites to look up, one per line. Plain domains work — `example.com` is fine, no need for `https://`. Link-in-bio hubs such as `linktr.ee/name` are accepted too and get resolved to the real site behind them. Paste the output of any other scraper straight in; duplicates of the same site are removed automatically.

## `resolveLinkHubs` (type: `boolean`):

If a line is a Linktree, Beacons, Stan, Taplink or similar hub, open it and audit the real website behind it instead. Without this the scraper reads the hub's own page and can return a sponsor's address as the business contact. Businesses whose whole presence turns out to be a hub are tagged `linktree_only`.

## `enrichSite` (type: `boolean`):

Reads the CMS, SSL, mobile-friendliness, copyright year, analytics tools and social links from the page already fetched — no extra requests, no extra time. This is what turns an address list into a work list: it tells you who actually needs what you sell.

## `maxContactPages` (type: `integer`):

If the homepage has no contact details, the scraper follows the contact link and probes the usual paths (/kontakt, /impressum, /contact …). Set to 0 to only read the homepage — faster and cheaper, but finds fewer e-mails.

## `onlyWithContact` (type: `boolean`):

Skips rows with neither e-mail nor phone. Leave off to keep a complete list including the misses — useful for judging coverage of your input.

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

Optional, and not needed for most sites. Some hosts — noticeably more often in the US — refuse requests coming from datacenter IP addresses and answer with 403. Those rows come back with `fetchStatus: http_403` and no contact data. A residential proxy usually gets through them.

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

Higher is faster. Lower is gentler on the sites you are visiting.

## `maxRunTimeSecs` (type: `integer`):

The run stops cleanly before this limit and keeps everything found so far.

## Actor input object example

```json
{
  "startUrls": [
    "apify.com",
    "brevo.com",
    "pipedrive.com"
  ],
  "resolveLinkHubs": true,
  "enrichSite": true,
  "maxContactPages": 5,
  "onlyWithContact": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "maxConcurrency": 10,
  "maxRunTimeSecs": 3000
}
```

# Actor output Schema

## `leads` (type: `string`):

One row per website: e-mail with its source, type and confidence, phone, social profiles, the site profile (CMS, SSL, mobile, age, analytics), and the two scores with a full breakdown of how each was reached.

## `runStatus` (type: `string`):

Outcome of the run: how many websites were processed, how many yielded an e-mail, the hit rate, and — if nothing was found — the reason why. An empty result never has to be guessed at.

# 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": [
        "apify.com",
        "brevo.com",
        "pipedrive.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("berkaydev/website-email-contact-scraper").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": [
        "apify.com",
        "brevo.com",
        "pipedrive.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("berkaydev/website-email-contact-scraper").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": [
    "apify.com",
    "brevo.com",
    "pipedrive.com"
  ]
}' |
apify call berkaydev/website-email-contact-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/LzDbehsYvb0N9fPGV/builds/VP6luN9h3fOATnPeN/openapi.json
