# Website Contact Extractor: Emails, Phones & Social Links (`humble-echidna/website-contact-extractor`) Actor

Get emails, phone numbers and social links (LinkedIn, Facebook, Instagram, X, YouTube, TikTok) from websites or a Google Maps dataset. Reads the home, contact and Impressum pages, normalizes phones to E.164, finds contact pages and forms. One row per site. Pay per page read.

- **URL**: https://apify.com/humble-echidna/website-contact-extractor.md
- **Developed by:** [Michael Costa](https://apify.com/humble-echidna) (community)
- **Categories:** Lead generation, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 page reads

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

### What does Website Contact Extractor: Emails, Phones & Social Links do?

**Website Contact Extractor** is an email extractor for websites: give it a list of sites, or a dataset from a Google
Maps or directory scraper, and get **one row per website** with its **emails**, **phone numbers in E.164**, **social
links** and contact page, read live from the sites' public pages. No login.

[Input fields](https://apify.com/humble-echidna/website-contact-extractor/input-schema) ·
[API](https://apify.com/humble-echidna/website-contact-extractor/api) ·
[Use it from Claude, ChatGPT or Cursor (MCP)](#can-i-use-website-contact-extractor-from-an-ai-agent-mcp)

**Jump to:** [Fields](#what-data-does-website-contact-extractor-return) ·
[Price](#how-much-does-it-cost-to-extract-contacts-from-websites) ·
[How to use](#how-to-extract-emails-and-phone-numbers-from-websites) ·
[Google Maps leads](#get-emails-for-your-google-maps-leads) ·
[Switching](#switching-from-another-actor) ·
[Input](#input) · [Output](#output) ·
[AI agents (MCP)](#can-i-use-website-contact-extractor-from-an-ai-agent-mcp) · [Limits](#limits) · [FAQ](#faq)

**Try it in one click:** the input comes pre-filled with one website. That's 5 pages, or about $0.01 (5 × $0.0019, plus
$0.00005 for the run start). **Then replace it with your own list.**

### What data does Website Contact Extractor return?

| Field | Example | Notes |
|---|---|---|
| `primaryEmail`, `emails` | `info@acme.com` | Every address found, the site's own domain first. Placeholders, image names and tracking keys are dropped. |
| `primaryPhone`, `phones` | `+13035550142` | Valid numbers only, in E.164. `tel:` links come first. |
| `phoneDetails` | `{"national": "(303) 555-0142", "type": "mobile", "source": "tel link", ...}` | Each number's international and national format, country, type and where it was found. |
| `linkedin`, `facebook`, `instagram`, `x`, `youtube`, `tiktok`, `pinterest`, `threads`, `telegram`, `whatsapp` | `https://www.linkedin.com/company/acme` | The profile the site links to most; `null` when it links to none. Share buttons, single posts and embedded videos are left out. |
| `socialLinks` | | Every profile link found. |
| `contactPageUrl`, `contactFormUrl` | `https://acme.com/contact` | The contact or legal-notice page, and a page with a message form. |
| `address` | `1 Main St, Denver, US` | From the site's structured data; `null` when it doesn't publish one that way. |
| `pagesVisited`, `pagesScanned` | `3` | Which pages were read, and so what the website cost. |
| `error`, `notes` | | Why a website couldn't be read (dead domain, robots.txt, bot protection), and pages that failed. |
| `sourceTitle`, `sourcePlaceId`, `sourceIndex`, `source...` | `"Rosie's Bakery"` | From the dataset item the website came from, plus any `keepFields`. |

The full list is under [Output](#output).

### How much does it cost to extract contacts from websites?

You pay per page read, and you see exactly which pages in `pagesVisited`, plus $0.00005 each time a run starts. Rows
are free.

| Apify plan | Free | Starter | Scale | Business and above |
|---|---|---|---|---|
| Per 1,000 pages | **$1.90** | **$1.90** | **$1.40** | **$1.00** |

- **What a website costs:** at most **Max pages per website** × the page price, $0.0095 at the default 5 pages on the
  Starter plan. After the start page, only pages likely to hold contact details are read, so a small site often takes
  fewer.
- **1,000 Google Maps leads, for example:** a measured run on 51 real business websites read 144 pages, 2.8 per
  website with the unreadable ones counted. At that rate 1,000 websites take about 2,800 pages, or **about $5.40** on
  Starter. With **Max pages per website** at 1 (home pages only) the most it can be is $1.90.
- **Caps:** **Max websites per run** in the input, and **Maximum cost per run** in the run options. No page is fetched
  that the maximum can't pay for.

**Never charged:** websites that can't be read, pages that fail or don't exist, pages robots.txt disallows, bot-check
pages, and the rows themselves.

### How to extract emails and phone numbers from websites

1. Open Website Contact Extractor and click **Try for free** (or **Start** if you're signed in).
2. Paste your websites into **Website URLs**, one per line (`acme.com` is enough), or pick a dataset.
3. Optional: set **Max pages per website**, and **Country for local phone numbers** if your sites' numbers are written
   without a country code and the sites have no country domain.
4. Click **Start**, then open the **Output** tab and export as JSON, CSV or Excel.

### Get emails for your Google Maps leads

Most Google Maps results give you a website but no email. Pick the Maps scraper's dataset in **Or: websites from a
dataset**: the `website` field is found by itself, and Maps links are never mistaken for the site. A chain's branches
sharing one website are read once. Each row carries the place's `sourceTitle`, `sourcePlaceId` and `sourceIndex` so you
can join it back, and each place's `countryCode` sets the country for its local phone numbers. Add `address`, `phone`
or `categoryName` under **Fields to keep from the dataset** to get them as `sourceAddress`, `sourcePhone` and
`sourceCategoryName`.

#### Run it automatically after your scraper finishes

1. On the scraper's **Integrations** tab, add **Website Contact Extractor** and set it to start when the run succeeds.
2. In **Actor input**, set **Or: websites from a dataset** to `{{resource.defaultDatasetId}}`. Apify replaces it with
   the dataset of the run that just finished, and that is what gives this run read access to it.
3. Save, then test it on one of the scraper's past runs.

**Fill in that field:** a run that gets the dataset id only in the integration's `payload` isn't given access to the
dataset (this actor runs with Apify's limited permissions, tested 2026-11-04). It then fetches nothing, charges
nothing, and its status message says what to set.

**Without the Integrations form**, create a [webhook](https://docs.apify.com/platform/integrations/webhooks) on
`ACTOR.RUN.SUCCEEDED` with `https://api.apify.com/v2/acts/humble-echidna~website-contact-extractor/runs` as its URL
and this payload template:

```
{"datasetId": {{resource.defaultDatasetId}}, "keepFields": ["address", "phone"]}
```

### Switching from another actor?

If you use another contact scraper, the settings map like this:

| Elsewhere | Here |
|---|---|
| Start URLs | **Website URLs** (`urls`), as plain lines rather than objects. Or `datasetId`, which most contact scrapers don't take. |
| Maximum pages per start URL | **Max pages per website** (`maxPagesPerSite`), 1 to 20, default 5. |
| Maximum link depth | Not needed: pages are picked by what their links say, not by depth. |
| Merge contacts | Always on: one row per website. For one row per address, turn on **One row per email address**. |
| `linkedIns`, `facebooks` and the like (lists) | One best profile per network (`linkedin`, `facebook`...), with every link in `socialLinks`. |
| `phones`, `phonesUncertain` | `phones` holds only numbers libphonenumber says are valid, in E.164. |
| Proxy and browser settings | None: pages are fetched directly, without a browser ([what that misses](#limits)). |
| Lead enrichment and email verification add-ons | Not included. There's nothing extra to switch off or pay for. |

The usual surprise with per-page pricing is one website costing hundreds of pages. Here the most a website can cost is
**Max pages per website** × the page price, and `pagesVisited` lists what you paid for.

### Input

| Field | What it does |
|---|---|
| **Website URLs** (`urls`) | The websites, one per line. A deeper address starts there. Ignored with a dataset. |
| Or: websites from a dataset (`datasetId`) | One of your Apify datasets, or `{{resource.defaultDatasetId}}` in an integration. |
| Field with the website (`datasetUrlField`) | The item field holding the website, e.g. `contact.website`. Empty finds it automatically. |
| Fields to keep from the dataset (`keepFields`) | More item fields to copy, up to 20; `address` comes out as `sourceAddress`. |
| Max pages per website (`maxPagesPerSite`) | 1 to 20, default 5, the start page included. |
| Country for local phone numbers (`defaultCountry`) | Two-letter code. Empty uses the Maps item's `countryCode`, the site's country domain, the page's language tag, else the United States. |
| One row per email address (`onePerEmail`) | One row per address instead of per website, adding `email`, `emailPageUrl`, `emailOnSiteDomain`, `emailIsRole`. Websites without an email give no row. |
| Max websites per run (`maxResults`) | Stop after this many websites. |

```json
{
  "urls": ["https://wikimediafoundation.org/", "acme.com"],
  "maxPagesPerSite": 5,
  "defaultCountry": "US"
}
```

### Output

The pre-filled input's row, from a real run on 2026-11-04 (lists and `phoneDetails` shortened):

```json
{
  "url": "https://wikimediafoundation.org/",
  "domain": "wikimediafoundation.org",
  "finalUrl": "https://wikimediafoundation.org/",
  "primaryEmail": "info@wikimedia.org",
  "emails": ["info@wikimedia.org", "donate@wikimedia.org", "business@wikimedia.org", "answers@wikimedia.org", "legal@wikimedia.org"],
  "primaryPhone": "+14158396885",
  "phones": ["+14158396885", "+14153478540"],
  "phoneDetails": [{"number": "+14158396885", "international": "+1 415-839-6885", "national": "(415) 839-6885", "country": "US", "type": "fixed line or mobile", "source": "page text"}],
  "linkedin": "https://www.linkedin.com/company/wikimedia-foundation",
  "facebook": "https://www.facebook.com/wikipedia",
  "instagram": "https://www.instagram.com/wikimediafoundation",
  "x": "https://x.com/wikimedia",
  "youtube": "https://www.youtube.com/@wikipedia",
  "tiktok": "https://www.tiktok.com/@wikipedia",
  "pinterest": null,
  "threads": null,
  "telegram": null,
  "whatsapp": null,
  "socialLinks": ["https://www.instagram.com/wikimediafoundation", "https://x.com/wikimedia"],
  "contactPageUrl": "https://wikimediafoundation.org/contact/",
  "contactFormUrl": null,
  "address": null,
  "phoneCountry": "US",
  "pagesVisited": ["https://wikimediafoundation.org/", "https://wikimediafoundation.org/contact/", "https://wikimediafoundation.org/who-we-are/", "https://wikimediafoundation.org/who-we-are/people/", "https://wikimediafoundation.org/role/executive/"],
  "pagesScanned": 5,
  "error": null,
  "notes": ["read the 5 pages allowed per website; 3 more candidate pages left"],
  "sourceTitle": null,
  "sourcePlaceId": null,
  "sourceIndex": null,
  "scrapedAt": "2026-11-04T22:28:59.598Z"
}
```

A website that can't be read still gets a row, with `error` saying why, empty lists, and `pagesScanned` 0. The run's
`RUN_STATS` record lists every website, what it gave and anything that failed.

### Run it on a schedule, or from your own code

1. Save your input as a [task](https://docs.apify.com/platform/actors/running/tasks) (**Save as a new task**, top right
   of the actor page) and add it to a [schedule](https://docs.apify.com/platform/schedules) (Console → Schedules →
   **Create new**).
2. Collect results: download the dataset as JSON, CSV or Excel; fetch the latest run's results from the
   [API](https://apify.com/humble-echidna/website-contact-extractor/api)
   (`GET https://api.apify.com/v2/actor-tasks/<task id>/runs/last/dataset/items?status=SUCCEEDED&format=csv`, with
   your API token); let a [webhook](https://docs.apify.com/platform/integrations/webhooks) tell your system when a
   run succeeds; or connect it to Make, Zapier or n8n through
   [Apify's integrations](https://docs.apify.com/platform/integrations).

#### Can I use Website Contact Extractor from an AI agent (MCP)?

Yes, through [Apify's MCP server](https://docs.apify.com/platform/integrations/mcp), from Claude, ChatGPT, Cursor or
any other MCP client. Add this to your client's MCP configuration (or let the agent find it with the server's actor
search); your client signs you in to Apify:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=humble-echidna/website-contact-extractor"
    }
  }
}
```

To use an [Apify API token](https://console.apify.com/settings/integrations) instead of signing in, add
`"headers": {"Authorization": "Bearer <APIFY_TOKEN>"}` next to `url`.

An agent passes for example `{"urls": ["acme.com"], "maxPagesPerSite": 3}`. It runs under Apify's limited permissions,
so it needs no approval step before an agent can start it.

### Who it's for

Anyone turning a list of businesses into a list of contacts: sales and agency teams enriching Google Maps or directory
leads on each scrape, recruiters adding contacts to company lists, and people building local business directories.

### Why this one?

- **A bill you can work out first.** The most a website can cost is Max pages per website × the page price, and each
  row lists the pages you paid for. There are no add-ons, and failed pages are free.
- **Built to run after a scraper.** It takes a dataset and a field name directly, so no export and re-import, and
  carries your source fields into each row.
- **Phones you can dial.** Every number is checked with libphonenumber and written in E.164, with its country, type and
  where it was found. Dates, opening hours and order numbers that merely look like numbers are filtered out.
- **Measured on real leads.** On 51 small and mid-size business websites from OpenStreetMap (Denver, Manchester,
  Hamburg), run on Apify on 2026-11-04: 37 could be read, and of those 32 gave an email, 33 a phone number and 26 a
  social profile. Of the other 14, 5 domains were dead, 5 sites' robots.txt asks crawlers not to read them, 3 refused
  the request and 1 didn't answer in time.
- **Polite and safe.** It identifies itself honestly (User-Agent `HumbleEchidnaApify`), follows each site's robots.txt
  and Crawl-delay, and only requests public web addresses on the standard ports (80 and 443).
- **Reliable.** One failing website never affects the others, and each failure is named in its row.

### Limits

- **No browser.** Pages are fetched as HTML, so details that only appear after scripts run are missed unless they're in
  the page source (Wix and Squarespace sites usually are). There are no iframes and no image or PDF reading.
- **No proxies, no bot-check workarounds.** A site that refuses automated requests or shows a bot check is reported, not
  worked around, and its robots.txt is followed. You'll get fewer sites read than from a crawler that ignores both.
- **Same site only:** the site you gave and its subdomains, up to 20 pages. It finds contact pages; it isn't a whole-site
  crawl for emails buried in old blog posts.
- **Emails aren't verified.** They're what the site publishes; run them through a verifier before a large mailing.
- **Local phone numbers depend on the country.** A number without a country code is read in the country from the order
  under [Input](#input). On a generic domain with no other hint that's the United States; set **Country for local phone
  numbers** if that's wrong.
- At most 20,000 dataset items and 10,000 websites per run. Up to 50 emails and 20 phone numbers per website.

### FAQ

#### Is it legal to extract contact details from websites?

It reads public pages you point it at, with no login, and follows each site's robots.txt. You choose the sites, and a
site's own terms of use may limit automated access, so check them for your use. Contact details can be personal data
(`jane.smith@acme.com`), even when published: how you store and use them falls under laws such as the GDPR, and cold
outreach under laws such as CAN-SPAM and the ePrivacy rules. You are responsible for having a lawful basis. If in doubt,
ask a lawyer.

#### Why did a website return nothing, or fewer contacts than I can see?

Check its row. `error` says why it couldn't be read at all, and `notes` lists pages that failed or a page limit that was
reached (raise **Max pages per website**). If the row has neither, the details probably appear only after scripts run,
or sit on a page the links didn't point to. Please open an issue with the website if it looks wrong.

#### Why is this phone number wrong?

Most likely the country: a local number was read in the wrong one. `phoneCountry` shows which was used, and
`phoneDetails` shows where each number came from. Set **Country for local phone numbers**, or use a dataset with
`countryCode`.

#### Can I get one row per email address?

Yes, turn on **One row per email address**. It costs the same, since pages are charged and rows are free.

### Related actors

| Actor | Use it when |
|---|---|
| [Dataset Transformer](https://apify.com/humble-echidna/dataset-transform) | You want the results cleaned up: only rows with an email, duplicates removed by email, as a CSV. |
| [Google Sheets Import & Export](https://apify.com/humble-echidna/google-sheets) | You want the contacts in a Google Sheet after each run. |
| [Website Technology Detector](https://apify.com/humble-echidna/tech-stack-detector) | You also want to know what each lead's website runs on. It takes the same dataset. |
| [Bulk URL & Broken Link Checker](https://apify.com/humble-echidna/url-checker) | You want to weed out dead websites first. |

### Feedback and support

Found a bug, or a site whose contacts it misses? Open an issue on the **Issues** tab with the website.

### Versions

Current version: **0.1**. See the Changelog tab for what changed in each version.

# Changelog

This Actor's version history is a separate document: https://apify.com/humble-echidna/website-contact-extractor/changelog.md

# Actor input Schema

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

The websites to read, one per line: a full address (https://www.example.com/) or just a domain (example.com, which means its https:// home page). A deeper address (https://example.com/locations/denver) starts there. Each website gives one row with its emails, phone numbers and social profiles. The same site typed twice is read once. Ignored when datasetId is set.

## `datasetId` (type: `string`):

One of your Apify datasets, picked here or by id, e.g. a Google Maps scraper's results: each item's website is read (urls is ignored). Items without a website and Google Maps links are skipped; a website shared by several items is read once. Rows carry sourceTitle, sourcePlaceId, sourceIndex and any keepFields; a Maps item's countryCode sets its phone country. Started by an integration with this empty: the finished run's dataset. Up to 20,000 items, read-only.

## `datasetUrlField` (type: `string`):

Only with datasetId: the item field that holds the website, e.g. `website`, or a dotted path such as `contact.website`. Leave empty (the default) to find it automatically: the first of website, url, domain, site and homepage that has a website in the first 100 items, then the same names one level down (contact.website).

## `keepFields` (type: `array`):

Only with datasetId: more fields of each dataset item to copy into its row, e.g. `address`, `phone`, `categoryName` from a Google Maps scraper (dotted paths like `contact.city` work too). Each comes out as source + its name, so `address` becomes `sourceAddress` and `contact.city` becomes `sourceContactCity`. Up to 20 fields; empty (the default) keeps only sourceTitle, sourcePlaceId and sourceIndex.

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

The most pages read on one website, the start page included, e.g. 5 (the default). Only pages likely to hold contact details are read after the start page: contact and legal-notice (Impressum) pages first, then about, team and location pages, then privacy and support pages. A site with fewer such pages is read in fewer. Each page read is one charged page. 1 to 20.

## `defaultCountry` (type: `string`):

Optional two-letter country code (US, GB, DE, FR, AU...) for phone numbers written without their country code, like (303) 555-0142 or 040 864171. Leave empty (the default) to use, in order: a Google Maps item's countryCode, the website's country domain (.de, .co.uk), the page's own language tag (en-GB), else the United States. Numbers written with + and their country code are always read as written.

## `onePerEmail` (type: `boolean`):

Off (the default): one row per website, with every email in `emails`. On: one row per email address found, each with email, emailPageUrl (where it was found), emailOnSiteDomain and emailIsRole (info@, sales@...) plus the website's other fields, for mailing tools and email verifiers. Websites without an email give no row in this mode. The price is the same: pages are charged, rows are free.

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

Stop after this many websites in total, e.g. 100. Leave empty (the default) for no limit. The run also stops cleanly at the maximum cost per run you set in the run options: no page is fetched that it can't pay for.

## Actor input object example

```json
{
  "urls": [
    "https://wikimediafoundation.org/"
  ],
  "keepFields": [],
  "maxPagesPerSite": 5,
  "onePerEmail": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `runStats` (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": [
        "https://wikimediafoundation.org/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("humble-echidna/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": ["https://wikimediafoundation.org/"] }

# Run the Actor and wait for it to finish
run = client.actor("humble-echidna/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": [
    "https://wikimediafoundation.org/"
  ]
}' |
apify call humble-echidna/website-contact-extractor --silent --output-dataset

```

## MCP server setup

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