# Website Contact Scraper — $3/1K Sites, Emails & Phones (`apifry/website-contact-extractor`) Actor

\[$3/1K sites with contacts] Website contact scraper and email extractor: paste company domains and get one row per site with emails, phones in E.164, LinkedIn, Facebook, Instagram and 6 more socials, the contact page and an MX check. Sites with no contacts are free. No API key.

- **URL**: https://apify.com/apifry/website-contact-extractor.md
- **Developed by:** [apifry](https://apify.com/apifry) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 website with contacts

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

## Website Contact Scraper — Emails, Phones & Socials

Paste a list of company websites and get **one clean lead row per website**: emails, phone numbers, social profiles and the contact page URL. No API keys, no login. A fast plain-HTTP crawl by default, plus an optional browser mode for websites that only show contacts after JavaScript runs.

**You only pay for websites where at least one email or phone number was found.** Sites with no contacts, unreachable sites and sites behind bot protection are still listed in the results (so you know what happened) but cost nothing.

### Tested on 100 real company websites

Same 100 websites, same day, compared with the most-used contact scraper in Apify Store (which charges per page), both limited to 8 pages per site:

| | This Actor | Per-page contact scraper |
|---|---|---|
| Websites with an email | **74** | 66 |
| Unique emails found | **587** | 339 |
| Placeholder / broken addresses | **0** | 3 |
| Price for these 100 websites | **$0.23** | $1.44 |

You pay for the 78 websites where contacts were found, not for 732 pages. Honest notes: one run on mostly mid-size and large companies (SaaS, hosting, law firms, hotels, restaurants, brands, nonprofits in the US and Europe); the other scraper got past bot protection on more sites (97 vs. 86 reachable); about a quarter of our emails are on the company's other domains or its partners' — turn on *Only emails on the website's own domain* if you don't want them.

### What you get for each website

| Field | Example |
|---|---|
| `domain` | `acme.de` |
| `status` | `ok`, `no_contacts`, `unreachable`, `blocked`, `invalid_input` |
| `primaryEmail` | `info@acme.de` — the best first-touch address |
| `emails` | `["info@acme.de", "anna.schmidt@acme.de"]` |
| `emailDetails` | type (`generic` / `personal`), same domain as the site, MX check, page where it was found |
| `phones` | `["+493012345670"]` — normalized to international E.164 format |
| `linkedin`, `facebook`, `instagram`, `twitter`, `youtube`, `tiktok`, `pinterest`, `telegram`, `whatsapp` | company profile URLs |
| `contactPageUrl` | `https://acme.de/kontakt` |
| `pagesScraped` | `3` |
| `renderedWithBrowser` | `false` — `true` if contacts were found only with the browser option |

Results can be downloaded as CSV, Excel or JSON, or sent to Google Sheets, HubSpot, Make, Zapier and n8n through Apify integrations.

### Why this extractor finds more (and less junk)

- **Goes straight to the right pages.** Besides the homepage it opens the pages most likely to list contacts — Contact, About, Team, Impressum, Mentions légales, Aviso legal and their equivalents in 10+ languages — instead of crawling a blog.
- **Reads hidden emails.** Cloudflare-protected addresses (`[email protected]`), `name [at] company [dot] com`, HTML entities and emails inside JSON/scripts.
- **Drops junk.** No `logo@2x.png`, tracking IDs, `noreply@`, `you@example.com` or placeholder addresses.
- **Checks deliverability.** Every email domain is checked for mail servers (MX), so dead addresses are flagged.
- **Phones you can dial.** Numbers from `tel:` links and page text are validated and converted to `+CountryCode…`. The country is guessed from the domain ending (`.de` → Germany) or set by you.
- **Handles real-world input.** `www.`, `http://`, paths and duplicates are normalized, redirects to a new domain are followed, international domains are supported.

### Use cases

- **Cold outreach.** You have a list of target companies from Google Maps, a directory or a trade-show exhibitor list. Get a generic address (`info@`, `sales@`) and named people's emails for each one, ready for your email tool.
- **CRM enrichment.** Export the websites of accounts with no contact data from HubSpot, Pipedrive or a spreadsheet, run them here and import the results back. One row per website keeps the join simple.
- **Partner and supplier lists.** Check hundreds of distributor, agency or supplier websites at once and see who publishes a contact email, a phone number or a LinkedIn page — and which websites are down.
- **List cleanup.** Spot dead domains (`unreachable`), websites that redirect to another company, and emails whose domain can no longer receive mail (`mxValid: false`) before you send a campaign.

### How to use it

1. Add websites to **Websites** — one domain or URL per line (thousands per run are fine).
2. Optionally adjust **Max pages per website** (default 8) and the other options.
   - Many websites came back `no_contacts` or `blocked`? Turn on **Render JavaScript for sites with no contacts (browser)**. Only those websites are opened again in a headless browser, and they are billed at the browser price only if contacts are found. Give the run at least 2 GB of memory.
3. Click **Start** and download the results.

#### Input example

```json
{
  "urls": ["acme.de", "https://www.example-bakery.co.uk/about"],
  "maxPagesPerDomain": 8,
  "includePhones": true,
  "includeSocials": true,
  "onlySameDomainEmails": false,
  "verifyMx": true,
  "renderJavaScript": false
}
```

#### Output example

```json
{
  "domain": "acme.de",
  "status": "ok",
  "primaryEmail": "info@acme.de",
  "emails": ["info@acme.de", "anna.schmidt@acme.de"],
  "phones": ["+493012345670"],
  "linkedin": "https://www.linkedin.com/company/acme-gmbh",
  "instagram": "https://www.instagram.com/acme.de",
  "contactPageUrl": "https://acme.de/kontakt",
  "pagesScraped": 3
}
```

### Pricing

| Event | When | Price |
|---|---|---|
| Website with contacts | Email or phone found with the normal (fast) crawl | **$3.00 / 1,000 websites** |
| Website with contacts (browser) | Contacts found only after rendering the site in a browser — only if you turn this option on | **$12.00 / 1,000 websites** |

Each website is billed at most once. Websites where nothing was found, unreachable or blocked websites are free — also when the browser was tried. The `renderedWithBrowser` field in the results shows which price applied. You can set a maximum spend per run in the run options; the extractor stops cleanly when the limit is reached.

### Use with the API, AI agents and MCP

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("<username>/website-contact-extractor").call(run_input={"urls": ["acme.de"]})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["domain"], item["primaryEmail"], item["phones"])
```

The extractor also works as a tool for AI agents through the [Apify MCP server](https://mcp.apify.com).

### FAQ

**Do I need proxies?** Usually not. Sites that answer with bot protection are automatically retried once through Apify Proxy. If many sites still come back as `blocked`, turn on the browser option (it gets past some bot checks) or Apify Proxy (Residential) for all requests in the input.

**What about very long lists?** Runs time out after 1 hour by default. Before the timeout the extractor stops cleanly and tells you how far it got; resurrect the run (Actions → Resurrect) and it continues where it stopped without charging twice for any website.

**Why is a website `no_contacts`?** The site doesn't publish an email or phone on its main pages (some only have a contact form), or loads its content with JavaScript only. You are not charged for it. For JavaScript-only sites, turn on the browser option and run those websites again.

**Is it legal?** See *Responsible use* below.

### Responsible use

The extractor only opens publicly available pages of the websites you list. It does not log in, solve CAPTCHAs or bypass paywalls, and it does not guess or generate email addresses. Personal data such as named employees' emails is still personal data: you are responsible for having a lawful basis to process it and for following the rules that apply to your outreach (for example GDPR in the EU/UK and CAN-SPAM in the US). If you are unsure, ask a lawyer.

### Support

Something missing, wrong or unclear? Open an issue on the **Issues** tab — include the website and what you expected to see. Issues are answered within one business day. Need a custom version (other fields, other sources, a scheduled pipeline)? Open an issue with the details too.

# Actor input Schema

## `urls` (type: `array`):

Domains or URLs of the websites to check, one per line. Examples: example.com, https://www.example.com/about. Duplicates (including www/non-www) are processed once.

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

The homepage plus the most likely contact pages (Contact, About, Impressum, Team, Legal…). Higher values find more contacts but take longer.

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

Extract phone numbers from tel: links and page text, normalized to E.164 (+4930123456).

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

LinkedIn, Facebook, Instagram, X/Twitter, YouTube, TikTok, Pinterest, Telegram and WhatsApp links.

## `onlySameDomainEmails` (type: `boolean`):

Drop emails on other domains (e.g. gmail.com or an agency's address). Off by default, because many small businesses use free mail providers.

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

Check that each email's domain can receive mail (has MX or A records). Emails that fail are kept but marked mxValid: false.

## `renderJavaScript` (type: `boolean`):

Off by default. When on, websites where the normal crawl found no email or phone, or that were blocked, are opened once more in a real browser (headless Chrome). This finds contacts on sites that only show them after JavaScript runs. Such sites are billed at the separate, higher 'domain-with-contacts-browser' price, and only if contacts are found. Slower: give the run at least 2048 MB of memory.

## `phoneCountry` (type: `string`):

Two-letter country code used to read local phone numbers without a + prefix (e.g. DE, GB, US). Leave empty to guess it per website: from the domain ending for country domains (.de, .co.uk), and for .com, .org, .io and similar from the pages themselves (language tag, numbers written with +country code, UK postcodes, country names), falling back to US. Set it when all your websites are from one country.

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

How many websites are processed in parallel.

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

How long to wait for a single page before giving up.

## `retryBlockedWithProxy` (type: `boolean`):

If a website answers with bot protection (HTTP 403/429), try it once more through Apify Proxy. Only blocked sites use the proxy.

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

Use a proxy for every request. Usually not needed: blocked sites are already retried through Apify Proxy (see the option above).

## Actor input object example

```json
{
  "urls": [
    "apify.com",
    "python.org",
    "crawlee.dev"
  ],
  "maxPagesPerDomain": 8,
  "includePhones": true,
  "includeSocials": true,
  "onlySameDomainEmails": false,
  "verifyMx": true,
  "renderJavaScript": false,
  "phoneCountry": "",
  "maxConcurrency": 20,
  "requestTimeoutSecs": 20,
  "retryBlockedWithProxy": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (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 = {
    "urls": [
        "apify.com",
        "python.org",
        "crawlee.dev"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("apifry/website-contact-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 = {
    "urls": [
        "apify.com",
        "python.org",
        "crawlee.dev",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("apifry/website-contact-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 '{
  "urls": [
    "apify.com",
    "python.org",
    "crawlee.dev"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call apifry/website-contact-extractor --silent --output-dataset

```

## MCP server setup

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