# Contact Email, Phone & Social Extractor (`headlessagent/contact-email-extractor`) Actor

Crawl company websites and extract contact emails (including mailto and simple obfuscations), phone numbers and social profiles. One clean dataset record per domain, with optional MX validation of email domains.

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

## Pricing

from $1.50 / 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.

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

## Contact Email, Phone & Social Extractor

Give this actor one or more company websites and it returns the contact details those sites publish: email addresses, phone numbers and links to their social profiles. Everything is deduplicated and grouped into one clean row per website, ready for a spreadsheet, a review list or your CRM. It is made for sales, recruiting and marketing people who need contact data without copying it by hand, and you do not need to write any code to use it.

### What you get

One row per website, even when you give it several links from the same site. Each row contains:

- **Email addresses** the site shows, including ones written as `name [at] example [dot] com` and ones a site hides behind its spam protection.
- **Phone numbers**, reduced to digits. Numbers with a country code keep the leading `+`.
- **Social profiles** on LinkedIn, X (Twitter), Facebook, Instagram, YouTube, TikTok, GitHub, Pinterest, Threads and Telegram. Only real profiles: share buttons, single posts and videos are left out.
- **Where each email and phone was found**, so you can check the source page yourself.
- **The pages that were visited**, without tracking parameters.
- **A list of pages that could not be opened**, with the reason.
- **The date and time** the row was made.

Obvious junk is filtered out, such as `noreply@` addresses, addresses at well-known placeholder domains and image file names.

### How to use

1. **Open the actor in Apify Console** and go to the **Input** tab. The form already contains one example address; replace it with yours.
2. **Fill in the form.** Start URLs and Domains decide what gets visited, so at least one of them must have something in it.

   - **Start URLs** — full web addresses, including `https://`, for example `https://www.example.com/contact`. Add as many as you need.
   - **Domains** — site names only, for example `example.com`. Useful when you do not know the exact contact page. Several are allowed.
   - **Max link depth** — how many links away from your starting page the actor may go. `0` visits only the pages you listed, `1` also visits pages linked from them, and so on. Allowed: 0 to 5. Empty means 2.
   - **Max pages per domain** — the most pages it will try to open on one website, counting the starting page. Allowed: 1 to 100. Empty means 10. Use 1 to read only the page you gave it.
   - **Validate email domains** — off by default. Turn it on to check that the part of each email after the `@` is set up to receive mail. Each email then gets a status: `has_mx` (can receive mail), `no_mx` (cannot) or `unknown` (could not tell). Empty or off means no check, and the status is `null`.

   You can fill in both **Start URLs** and **Domains**; the two lists are combined and duplicates are removed.

   If **Start URLs** and **Domains** are both empty, the run stops right away with a message and you get no data. A mistyped address also stops the run, and the message names the address that needs fixing.
3. **Press Start.**
4. **Wait for the run to finish.** The log shows pages as they are read. Sites with many pages take longer; the page limit keeps a single site short.
5. **Open the Output tab** (the dataset). Each row is one website. Review the emails and phones, then use **Download** to save the rows as JSON, CSV or Excel.
6. If you prefer, the same rows can be fetched through the Apify API or passed to another tool from the Integrations tab, without any code.

### Example

#### Input

```json
{
  "startUrls": [{ "url": "https://www.apify.com/contact" }],
  "domains": [],
  "maxDepthLinks": 0,
  "maxPagesPerDomain": 1,
  "validateEmails": true
}
```

#### Output

One row of the output. Empty networks are shown here because they are always part of a row.

```json
{
  "domain": "apify.com",
  "startUrl": "https://apify.com/contact",
  "crawledAt": "2026-10-27T05:57:48.611Z",
  "pagesCrawled": 1,
  "pages": [
    "https://apify.com/contact"
  ],
  "emails": [
    {
      "address": "hello@apify.com",
      "sourceUrls": [
        "https://apify.com/contact"
      ],
      "mxStatus": "has_mx"
    },
    {
      "address": "support@apify.com",
      "sourceUrls": [
        "https://apify.com/contact"
      ],
      "mxStatus": "has_mx"
    }
  ],
  "phones": [],
  "socialLinks": {
    "linkedin": [
      "https://linkedin.com/company/apify"
    ],
    "twitter": [
      "https://x.com/apify"
    ],
    "facebook": [],
    "instagram": [],
    "youtube": [
      "https://youtube.com/apify"
    ],
    "tiktok": [
      "https://tiktok.com/@apifyoffice",
      "https://tiktok.com/@apifytech"
    ],
    "github": [
      "https://github.com/apify"
    ],
    "pinterest": [],
    "threads": [],
    "telegram": []
  },
  "errors": []
}
```

This row comes from a real run of the actor.

If a page could not be opened, it appears in `errors` with the page address and a short reason.

### Tips and limits

- **Only what is visible on the page.** Contact details that appear only after pressing a button, inside a form, or inside an image are missed. Pages whose text is added later can also come up empty.
- **Some sites refuse automated visits.** Those pages are listed under `errors` and the run continues. If every page of a site is refused, you still get a row, but its lists may be empty.
- **One website at a time.** Links to other sites and to subdomains are not followed. `shop.example.com` and `example.com` are two separate websites, each with its own row.
- **Some pages are skipped.** Login, account, cart, checkout, admin and search pages are not opened, and each site's rules for automated visits (`robots.txt`) are respected.
- **Runs are gentle, so they can be slow.** Pages are read a few at a time. Each run stops automatically after one hour by default; for a long list, split it into smaller runs or raise the limit in the run options.
- **Phone numbers are a best guess.** Numbers wrapped across lines, hidden inside codes, or written without separators may be missed or misread.
- **The email check is not a mailbox test.** A `has_mx` result means the domain is set up to receive mail. It does not prove the address exists, and no message is ever sent.
- **Published sample addresses are kept.** If a site itself publishes a demo address, it is collected like any other address.
- **Set a realistic page limit.** A low limit keeps runs quick and polite; a high one finds more, especially on large sites.

#### Use it responsibly

- The actor reports contact details that websites publish openly, and these can include personal data such as personal mailboxes and direct phone numbers.
- If you save or use the results, you are responsible for following privacy law, for example GDPR/UK GDPR and CCPA: have a lawful basis, collect only what you need, keep it only as long as needed, and honor deletion requests.
- Follow anti-spam law too, for example CAN-SPAM and the ePrivacy rules: contact only people you may contact, honor opt-outs, and do not send unsolicited bulk email.
- Visit only what you are allowed to visit. Do not use the actor to get past logins, paywalls or other protections, and do not use it to test whether a mailbox exists.
- The sample row in this README comes from a real run of the actor.

### FAQ

**Do I need to know how to code?** No. Fill in the form, press Start, and download the rows.

**How many websites can I do in one run?** As many as you paste. Time grows with every website, and the default run limit is one hour, so split very long lists into a few runs.

**Why does a row have no emails?** The site may not publish any, may show them only inside a form or after pressing a button, or may have refused the visit. Check the `pages` and `errors` lists to see what happened.

**Does “can receive mail” mean the address is real?** No. It only means the domain is set up to receive email. The actor never contacts a mail server, so it cannot confirm that a particular mailbox exists.

**Why did two of my links give one row?** Because they belong to the same website: results are merged per site, and `www.` is ignored. Use two different sites, or two different subdomains, to get two rows.

# Actor input Schema

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

Full http(s) URLs to start from. One record is produced per unique domain, even when several start URLs share a domain.

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

Bare domains, for example "example.com" or "www.example.com". They are crawled over https:// (http:// is used automatically when https:// is unreachable). Joined with Start URLs and deduplicated.

## `maxDepthLinks` (type: `integer`):

How many link levels to follow from each start URL. 0 crawls only the start pages, 1 follows links found on them, and so on. Contact, about, team and legal pages are always crawled before other links.

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

Hard cap on HTML pages crawled for a single domain, including start pages. Lower it to stay extra polite, raise it for large corporate sites.

## `validateEmails` (type: `boolean`):

When enabled, every unique email domain is checked for MX records via DNS. No SMTP probing and no messages are sent. Adds one field to each email: mxStatus = has_mx, no_mx or unknown.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.apify.com"
    }
  ],
  "domains": [],
  "maxDepthLinks": 2,
  "maxPagesPerDomain": 10,
  "validateEmails": false
}
```

# Actor output Schema

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

Dataset with one record per domain: crawled pages, unique emails with optional MX status, phone numbers, social profiles and crawl errors.

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

// Run the Actor and wait for it to finish
const run = await client.actor("headlessagent/contact-email-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 = {
    "startUrls": [{ "url": "https://www.apify.com" }],
    "domains": [],
}

# Run the Actor and wait for it to finish
run = client.actor("headlessagent/contact-email-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 '{
  "startUrls": [
    {
      "url": "https://www.apify.com"
    }
  ],
  "domains": []
}' |
apify call headlessagent/contact-email-extractor --silent --output-dataset

```

## MCP server setup

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