# Website Contact Scraper - Emails, Phones & Socials (`jmlp/web-contact-scraper`) Actor

Extract public contact details from any list of websites. Finds emails, international phone numbers and LinkedIn, X, Facebook, Instagram, YouTube, TikTok and GitHub profiles by reading the pages that actually carry them - contact, about, team and imprint. No login or API key.

- **URL**: https://apify.com/jmlp/web-contact-scraper.md
- **Developed by:** [Mary Lou](https://apify.com/jmlp) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 2 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.15 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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?

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 — emails, phone numbers and social profiles

Give it a list of websites. Get back the **email addresses, phone numbers and
social media profiles** those companies publish — ready for your CRM, your
outreach list or your enrichment pipeline.

An email scraper, phone number extractor and social profile finder in one, and
it is built around precision: the hard part of contact scraping is not finding
strings that look like emails, it is **not returning the junk**.

No login, no API key, no browser. Paste domains and press Start.

***

### What you can do with it

- **B2B lead generation** — turn a list of company domains into contactable
  leads
- **CRM enrichment** — fill in missing contact details for accounts you already
  have
- **Sales prospecting** — take a list from any source and get the contact layer
- **Recruitment outreach** — find how to reach the companies you are targeting
- **Agency and partner research** — build a contact sheet for a whole market
- **Data cleaning** — re-check a stale contact list and see what has changed
- **Pair it with a search or jobs scraper** — collect domains there, contacts
  here

***

### How to scrape contact details

**1.** Paste your websites into **Websites**, one per line. `https://` is
optional — `example.com` works.

**2.** Press Start. Every other field has a sensible default.

A list of 500 domains at default settings takes a few minutes.

***

### It reads the right pages, not every page

A contact scraper that spiders an entire website burns a hundred requests to
find an address that was in the footer of the homepage. This one scores every
internal link and visits only the pages that actually carry contact details,
shallowest first:

| Priority | Page types |
| --- | --- |
| Highest | `/contact`, `/contact-us`, `/kontakt`, `/contacto`, `/contatti` |
| | `/impressum`, `/imprint`, `/legal-notice`, `/mentions-legales` |
| | `/about`, `/about-us`, `/company`, `/quienes-somos`, `/ueber-uns`, `/over-ons` |
| | `/team`, `/people`, `/staff`, `/leadership`, `/management` |
| | `/support`, `/help`, `/customer-service` |
| Lowest | `/press`, `/media`, `/newsroom`, `/privacy`, `/datenschutz` |

Default is 8 pages per site, which finds almost everything. Raise **Max pages
per site** for sprawling corporate sites.

Multi-language page names are recognised, so German, French, Spanish, Italian
and Dutch sites work as well as English ones.

***

### Input

| Field | What it does |
| --- | --- |
| **Websites** | The domains to scan, one per line |
| **Max pages per site** | How deep to go on each site. Default 8 |
| **Max results** | Hard cap on how many websites are delivered |
| **Skip sites with no contacts** | Leave empty sites out of the dataset entirely |
| **Max concurrency** | How many sites to scan at once. Default 5 |
| **Delay between pages** | Politeness pause within a single site |
| **Proxy** | Optional. Most sites serve their contact page to anyone |

***

### Output

One row per website. Export as **JSON, CSV, Excel, XML or RSS**.

```json
{
  "url": "https://apify.com/",
  "domain": "apify.com",
  "emails": ["hello@apify.com", "privacy@apify.com"],
  "phones": ["+14242545300"],
  "socials": [
    "https://linkedin.com/company/apify",
    "https://x.com/apify",
    "https://youtube.com/c/Apify",
    "https://github.com/apify"
  ],
  "socials_by_network": { "linkedin": ["https://linkedin.com/company/apify"] },
  "has_contact": true,
  "pages_crawled": 4,
  "pages": ["https://apify.com/", "https://apify.com/contact"],
  "errors": []
}
```

| Field | Notes |
| --- | --- |
| `domain` | Lowercased, no `www.`. This is the dedup key — one row per domain |
| `emails` | Deduplicated and validated. See the filtering below |
| `phones` | E.164-style digits, from `tel:` links and page text |
| `socials` | Canonical profile URLs, one entry per profile |
| `socials_by_network` | The same profiles grouped, for when you only want the LinkedIn |
| `has_contact` | False when the site yielded nothing at all |
| `pages` | Exactly which URLs were read, so any result traces back to its source |
| `errors` | Pages that could not be read, with the reason |

Networks covered: **LinkedIn, X/Twitter, Facebook, Instagram, YouTube, TikTok,
GitHub**.

***

### What makes the output usable

Most contact scrapers hand you a pile of junk to clean up. Every rule below
exists because a real page produced a real false positive:

**Not emails:**

- `icon@2x.png`, `font@latin.woff2` — sprite and asset filenames parse as
  addresses
- Sentry DSNs — every site using Sentry has one whose userinfo looks like an
  email
- `you@example.com`, `name@domain.com` — template placeholders
- `...our service@scale.while others...` — prose running into an address.
  Caught by validating the top-level domain against reality rather than
  trusting the regex

**Not phone numbers:** years, prices, VAT numbers, product SKUs and timestamps.
A number has to be 8–15 digits after normalisation and not be `0000000000`.

**Not four copies of one profile.** Social links arrive escaped inside
JavaScript and JSON (`...\/company\/acme\\`) and with punctuation stuck to them
from surrounding prose. Every link is unescaped and canonicalised — `apify.com`
alone produced six spellings of the same LinkedIn page before this was fixed.

**Not the site it redirected to.** A link on a company's own page can redirect
anywhere. A naive crawler follows it and quietly returns Google's contact
details instead of the company's.

**Not third parties quoted in a blog post.** The crawler stays on pages about
the company itself, so a competitor mentioned in an article does not end up in
your row.

***

### Pricing

Pay per website delivered, plus Apify platform usage.

Worth comparing carefully, because **a result here is a whole website, not a
page**. The widely used alternative charges per 1,000 *pages*, and this crawler
reads up to 8 pages per site — so on a typical site you pay a fraction of what
per-page pricing costs, for the same answer.

Platform usage is billed by Apify at your own plan rate and is small here:
**about $0.08 per 1,000 websites** measured, since no proxy is needed by
default.

Two ways to keep it tight:

- **Sites that could not be loaded at all** — a dead domain, a DNS failure —
  are reported as failures and are **not** charged as results.
- Turn on **Skip sites with no contacts** and you are only charged for websites
  that actually yielded something.

Set **Max results** to cap any single run exactly.

***

### FAQ

**Is scraping contact details legal?**
It reads pages that companies publish deliberately, with no login and no
paywall — the same pages a person visits to find a phone number. Business
contact details are exactly what a `/contact` page is for. What you then *do*
with the data is governed by GDPR, CAN-SPAM and similar rules, and that part is
your responsibility: have a lawful basis before you email anyone.

**Do I need a proxy?**
Usually not. Most sites serve their own contact page to anyone. Turn one on if
you are scanning thousands of domains or hitting a WAF.

**Why did a site come back empty?**
Either it genuinely publishes no contact details, or they are rendered by
JavaScript, or they are inside an image to defeat scrapers. Check `pages` to
see what was read and `errors` for anything that failed.

**Can it find personal emails of specific employees?**
No. It returns what a company publishes on its own website. It does not guess
addresses from name patterns and it does not search anywhere else.

**Does it work on non-English websites?**
Yes. `impressum`, `kontakt`, `contacto`, `contatti`, `over-ons`,
`quienes-somos` and others are all recognised page names.

**How many pages does it read per site?**
8 by default, chosen by relevance rather than crawl order. Raise it for large
corporate sites.

**How fast is it?**
Measured: 4 sites in 5 seconds at concurrency 4, including one dead domain
correctly reported as a failure.

**Can I run it on a schedule?**
Yes. Re-check a domain list weekly and diff the results to catch changes.

***

### Related scrapers

- **LinkedIn Jobs Scraper** — job postings without a login or cookie. Companies
  that are hiring are companies worth contacting
- **Google Search Scraper** — build domain lists from `site:` and intent
  queries, then enrich them here
- **TikTok Ad Library Scraper**, **Meta Ads Library Scraper**, **Google Ads
  Transparency Center Scraper** — competitor ad intelligence

# Actor input Schema

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

The websites to scan. One per line, with or without https://. Each site is visited independently, so a list of 500 domains is a normal input. Accepts plain strings or {"url": "..."} objects.

## `url` (type: `string`):

A single website, for convenience when you only have one.

## `maxPagesPerSite` (type: `integer`):

How many pages to read on each site. The crawler does not spider blindly: it scores every internal link and visits only the page types that carry contact details - contact, imprint, about, team, support - shallowest first. 8 finds almost everything; raise it for large corporate sites.

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

Stop after this many sites have been delivered. Caps both runtime and cost. Empty = every site in the list.

## `skipSitesWithoutContacts` (type: `boolean`):

Leave a site out of the dataset entirely when nothing was found, instead of writing an empty row. Under pay-per-result pricing this means you are not charged for the misses.

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

How many sites to scan at once. Different sites are unrelated hosts, so this parallelises cleanly - 5 is polite, 20 is fast.

## `delayMs` (type: `integer`):

Politeness pause between requests to the SAME site. Does not slow down different sites running in parallel.

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

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

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

Optional. Most sites are happy to serve their own contact page to anyone, so a proxy is not needed for typical lists. Use one if you are scanning thousands of sites or hitting a WAF.

## `resume` (type: `boolean`):

Save progress every ~30s so a run that gets migrated or restarted by the platform picks up where it stopped, without repeating sites.

## `continueFromLastRun` (type: `boolean`):

If your previous run with the same input was interrupted, scan only the sites it missed. Those already done stay in THAT run's dataset.

## `impersonate` (type: `string`):

Which browser TLS fingerprint to present. Only matters for sites behind a WAF that refuses plain HTTP clients.

## Actor input object example

```json
{
  "startUrls": [
    "https://apify.com",
    "https://www.iana.org"
  ],
  "maxPagesPerSite": 8,
  "skipSitesWithoutContacts": false,
  "maxConcurrency": 5,
  "delayMs": 300,
  "requestTimeoutSecs": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "resume": true,
  "continueFromLastRun": false,
  "impersonate": "chrome131"
}
```

# Actor output Schema

## `contacts` (type: `string`):

One row per website: every email, phone number and social profile found, and which pages they came from.

## `summary` (type: `string`):

How many sites were scanned, how many yielded an email, a phone or a social profile, and which ones failed to load.

# 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": [
        "https://apify.com",
        "https://www.iana.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jmlp/web-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": [
        "https://apify.com",
        "https://www.iana.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("jmlp/web-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": [
    "https://apify.com",
    "https://www.iana.org"
  ]
}' |
apify call jmlp/web-contact-scraper --silent --output-dataset

```

## MCP server setup

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