# Website Contact Scraper — Verified Emails, Phones & Socials (`pabloalcolea02/website-contact-scraper`) Actor

Turn a list of domains into verified B2B contacts. Decodes Cloudflare-protected and obfuscated emails, validates MX, and never charges for domains where nothing is found.

- **URL**: https://apify.com/pabloalcolea02/website-contact-scraper.md
- **Developed by:** [PABLO ALCOLEA AGUILAR](https://apify.com/pabloalcolea02) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 domain enricheds

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

## Website Contact Scraper — Verified Emails, Phones & Socials

Give it a list of domains. Get back verified business emails, phone numbers in E.164,
social profiles, company name, postal address and VAT/company registration numbers.

**You are only charged for domains where something is actually found.** Dead domains,
parked pages and sites with no contact data cost you nothing.

***

### Why this one

Most contact extractors run a regex over the homepage and stop there. That misses the
majority of real contact data, because businesses hide their addresses on purpose.
This Actor handles what the others skip:

| | Typical extractor | This Actor |
|---|---|---|
| `mailto:` links | ✅ | ✅ |
| Plain text emails | ✅ | ✅ |
| **Cloudflare-protected emails** (`data-cfemail`) | ❌ | ✅ decoded |
| **`info [at] company [dot] com`** | ❌ | ✅ decoded |
| **HTML-entity encoded** (`&#105;&#110;&#102;&#111;…`) | ❌ | ✅ decoded |
| **Built by JavaScript** (`'info' + '@' + 'site.com'`) | ❌ | ✅ decoded |
| Contact / imprint / about pages | homepage only | ✅ found automatically |
| MX validation | ❌ | ✅ per email |
| Role vs personal mailbox | ❌ | ✅ classified |
| VAT / company number | ❌ | ✅ extracted |
| Billing on empty results | you pay anyway | **you don't pay** |

#### It finds the pages, you don't have to

You pass `example.com`. The Actor reads the homepage, then automatically locates and
reads the **contact**, **legal/imprint**, **about** and **team** pages — in English,
Spanish, German, French, Italian, Portuguese and Dutch. That is where the phone numbers
and the VAT number actually live.

***

### Input

```json
{
  "domains": ["basecamp.com", "hetzner.com", "https://posthog.com"],
  "maxPagesPerDomain": 4,
  "verifyMx": true,
  "includePersonalEmails": false
}
```

Paste domains one per line, or connect the `startUrls` input to another Actor — for
example, feed it the websites from a Google Maps scraper to turn a list of businesses
into a list of reachable contacts.

#### Options that matter

| Option | Default | What it does |
|---|---|---|
| `maxPagesPerDomain` | `4` | Homepage + up to 3 auto-discovered contact pages. Set to `1` for the cheapest, fastest pass. |
| `verifyMx` | `true` | DNS MX lookup per email domain. Undeliverable domains get flagged, not silently returned. |
| `includePersonalEmails` | `true` | Set to `false` to return **only** generic mailboxes (`info@`, `sales@`). The safer setting for GDPR-conscious outreach in the EU. |
| `minConfidence` | `0` | Every email is scored 0–100. Keep `0` and filter later, or raise it to only keep strong hits. |
| `defaultPhoneRegion` | auto | Read local-format phone numbers correctly. Inferred from the TLD if empty. |

***

### Output

One record per domain. This is a real, unedited result for `hetzner.com`, trimmed to
the first entry of each list:

```json
{
  "domain": "hetzner.com",
  "status": "success",
  "bestEmail": "info@hetzner.com",
  "bestPhone": "+499831505216",
  "company": {
    "name": "Hetzner Online GmbH",
    "address": "Industriestr. 25, 91710 Gunzenhausen",
    "legalIds": [{ "type": "Handelsregister", "value": "HRB6089", "country": "DE" }],
    "language": "en-US"
  },
  "emails": [
    {
      "email": "info@hetzner.com",
      "type": "role",
      "domainMatch": true,
      "isFreeProvider": false,
      "isDisposable": false,
      "hasMx": true,
      "mailProvider": "other",
      "confidence": 100,
      "foundVia": "mailto",
      "foundOn": "https://www.hetzner.com/legal/legal-notice/"
    }
  ],
  "phones": [
    { "phone": "+499831505216", "national": "09831 505216", "country": "DE", "type": "UNKNOWN", "source": "text-intl" }
  ],
  "socials": [
    { "network": "facebook", "handle": "hetzner.de", "url": "https://www.facebook.com/hetzner.de" }
  ],
  "counts": { "emails": 7, "phones": 3, "socials": 8, "pages": 4 },
  "durationMs": 1132
}
```

Note where the good data came from: the company name, the registered address and the
`HRB` registration number are **not on the homepage**. They are on the legal notice
page, which the Actor found and read on its own.

#### Reading the fields that decide quality

- **`confidence` (0–100)** — combines *how* the email was found (a `mailto:` beats a
  string in raw HTML), whether the email domain matches the site, whether the domain
  accepts mail, and the mailbox type. Sort by it and work top-down.
- **`type`** — `role` (`info@`, `sales@`) · `personal` (a named individual) ·
  `noreply` (excluded by default) · `other`.
- **`domainMatch`** — `true` means the email belongs to the site you asked about. `false`
  usually means a Gmail address or an agency's address, still useful but worth checking.
- **`hasMx`** — `false` means that domain has no mail server. Sending there will bounce.
- **`status`** — `success` (billed) · `no_contacts` (not billed) · `failed` (not billed).

***

### Pricing

**$5.00 per 1,000 enriched domains** — $0.005 per domain where at least one email,
phone or social profile is found.

**Domains with no results do not pay the enrichment fee.** Dead domains, parked pages
and sites with no contact data are not charged the $0.005. They still appear in your
dataset, marked `no_contacts` or `failed`, so you can see what happened to every input.

The full cost breakdown, with nothing hidden:

| Charge | Price | When |
|---|---|---|
| Domain enriched | **$5.00 / 1,000** | Only when contacts are found |
| Dataset record | $0.01 / 1,000 | Every row written, including unenriched ones |
| Actor start | $0.00005 per GB | Once per run |

So a run over 1,000 domains where 800 return contacts costs about **$4.01**, not $5.00 —
the 200 empty ones cost $0.002 in total instead of $1.00.

Duplicate inputs are removed before processing, so the same domain is never billed twice
in one run. No monthly rental, no subscription.

> The last two lines are Apify platform events that every Actor on the Store carries and
> that cannot be switched off. They are listed here rather than left for you to discover
> on the invoice.

***

### Good use cases

- **Enriching a scraped business list.** Google Maps and directory scrapers give you a
  website but rarely an email. This closes that gap.
- **Cleaning a CRM.** Feed in your account domains, get back current addresses, phone
  numbers and whether the mail domain still resolves.
- **Building a prospect list from a domain list** you already own — conference exhibitor
  lists, portfolio pages, industry directories.
- **Compliance/vendor checks.** VAT and company registration numbers are pulled from the
  imprint or legal notice page.

### What it does *not* do

Stated plainly so you don't waste a run:

- It does **not** guess or pattern-generate email addresses. Everything returned was
  actually published on the site.
- It does **not** log into anything, and it does not touch LinkedIn, Apollo or any
  platform that requires an account.
- It does **not** perform SMTP handshakes. `verifyMx` confirms the domain accepts mail;
  it does not confirm an individual mailbox exists.
- It does **not** render JavaScript. Sites that build their entire contact page
  client-side may return less. `maxPagesPerDomain` and the imprint fallback cover most
  of these, but not all.

***

### Notes on lawful use

This Actor reads publicly published pages on companies' own websites. It respects a
fixed timeout, fetches at most a handful of pages per domain, and never bypasses logins
or paywalls.

Business contact data is still regulated. In the EU/UK, using personal contact data for
marketing requires a lawful basis under GDPR, and you must honour opt-outs. Set
`includePersonalEmails` to `false` to keep only generic company mailboxes, which
substantially reduces personal-data exposure. You remain the data controller for
anything you collect and send.

***

### Pairs with

**[Email List Cleaner](https://apify.com/pabloalcolea02/email-list-cleaner)** — takes the
emails this Actor found and strips out the ones that will bounce: dead domains,
disposables, typos, plus role and no-reply flags. $0.31 per 1,000 addresses.

Build the list here, clean it there, then send.

***

### Support

Found a site where extraction misses something it shouldn't? Open an issue on the
Actor's Issues tab with the domain — extraction rules are updated from real cases.

# Actor input Schema

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

One domain or URL per line. Both `example.com` and `https://example.com/about` work. Duplicates are removed automatically so you are never billed twice for the same site.

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

Alternative input for integrations and for chaining from another Actor (for example a Google Maps scraper). Accepts the standard Apify request list format.

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

Pages fetched per site. Page 1 is always the homepage; the rest are the contact, legal/imprint, about and team pages, picked automatically. 4 is the sweet spot between hit rate and speed. Set to 1 to only read the homepage.

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

Runs a DNS MX lookup for every email domain found. Emails on domains with no mail server are flagged with `hasMx: false` and scored down, so you don't burn sender reputation on undeliverable addresses.

## `minConfidence` (type: `integer`):

Drops emails below this score. 0 keeps everything (recommended — every email carries its own score, so you can filter later). 60 keeps only strong matches.

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

Keeps addresses that look like a named individual (`maria.lopez@…`). Turn this OFF to return only generic company mailboxes (`info@`, `sales@`, `support@`), which is the safer default for GDPR-conscious outreach in the EU.

## `includeNoreply` (type: `boolean`):

Off by default. `no-reply@`, `postmaster@` and `bounce@` are never useful for outreach.

## `defaultPhoneRegion` (type: `string`):

ISO country code (ES, DE, FR, US…) used to read phone numbers written without an international prefix. Leave empty to infer it from the domain's TLD.

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

Domains processed in parallel. Higher is faster but uses more memory; 10 is a good default for a 1 GB run.

## `requestTimeoutSecs` (type: `integer`):

Seconds to wait for a single page before giving up on it.

## Actor input object example

```json
{
  "domains": [
    "example.com"
  ],
  "maxPagesPerDomain": 4,
  "verifyMx": true,
  "minConfidence": 0,
  "includePersonalEmails": true,
  "includeNoreply": false,
  "defaultPhoneRegion": "ES",
  "maxConcurrency": 10,
  "requestTimeoutSecs": 12
}
```

# 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": [
        "basecamp.com",
        "posthog.com",
        "hetzner.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pabloalcolea02/website-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 = { "domains": [
        "basecamp.com",
        "posthog.com",
        "hetzner.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("pabloalcolea02/website-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 '{
  "domains": [
    "basecamp.com",
    "posthog.com",
    "hetzner.com"
  ]
}' |
apify call pabloalcolea02/website-contact-scraper --silent --output-dataset

```

## MCP server setup

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