# Website Contact Extractor: Emails, Phones & Socials (`dodiabak/website-contact-extractor`) Actor

Turn any list of websites into verified contact profiles: emails (MX-checked, role vs personal), phone numbers validated to E.164, WhatsApp, social profiles, address, VAT ID and contact page. Reads homepage + contact/imprint pages. $3 per 1,000 sites.

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

## Pricing

from $3.00 / 1,000 website profileds

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/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 Extractor: Emails, Phones & Socials

**Turn a list of websites into clean, verified contact profiles: one row per company.** For every domain the Actor reads the homepage plus the pages that matter (contact, Impressum/imprint, about, team, legal) and returns:

- 📧 **Emails**, MX-verified and classified as **role** (info@, sales@), **personal** (first.last@) or generic, with on-domain / free-provider / disposable flags. Cloudflare-protected and `[at]`/`[dot]`-obfuscated addresses are decoded.
- 📞 **Phone numbers validated with Google's libphonenumber** and formatted to **E.164** (`+4971140040990`), with type (mobile, fixed line, toll-free), country, and labels for **fax** and **WhatsApp**.
- 🔗 **Social profiles**: LinkedIn, X/Twitter, Facebook, Instagram, YouTube, TikTok, GitHub, Pinterest, Threads, Telegram, WhatsApp, Discord, Crunchbase. Share buttons and junk links are skipped.
- 🏢 **Company data** from schema.org JSON-LD: name, legal name, postal address, geo coordinates, opening hours, VAT ID, founding date, logo.
- 🧭 The **contact page URL**, pages with a **contact form**, language and country.

**$3 per 1,000 websites** ($0.003 per profiled site). Unreachable or blocked sites are free. No browser and no proxies needed, so runs are fast (about 10 websites in parallel, 2–4 seconds each) and cheap.

### Why this one?

| | This Actor | Typical contact scrapers |
| --- | --- | --- |
| Phone numbers | **Validated + E.164 + type**, fax/WhatsApp labelled, dates/order numbers rejected | Raw regex matches, many false positives |
| Emails | **MX-checked**, role/personal split, obfuscation + Cloudflare decoding | Raw list, includes `image@2x.png` junk |
| Output | **One clean row per company**, with `primaryEmail` and `primaryPhone` picked for you | One row per page |
| Price | **Flat $0.003 per website** | $0.002+ per page (6 pages ≈ $0.012) |
| Etiquette | Respects robots.txt for sub-pages, reads at most N pages | Varies |

### Use cases

- **Lead generation and enrichment:** you have domains from Google Maps, a CRM export, a directory or a trade-show list, and you need emails, phones and LinkedIn pages.
- **CRM hygiene:** refresh old records, catch dead email domains, standardize phone formats.
- **AI sales agents:** one MCP call returns a structured profile the agent can act on.
- **Market research:** map companies' social presence and locations.

### How to use it

1. Paste websites into **Websites**, one per line: `acme.com`, `https://www.acme.de/`, or an email such as `jane@acme.com` (its domain is used).
2. Optional: set **Default country** for local phone numbers (otherwise it's detected from the address, country domain or page language), or turn off personal emails.
3. Click **Start**. Use the **Overview** or **Leads (flat)** view, or export CSV, Excel or JSON, or use the API.

#### Input example

```json
{
  "websites": ["apify.com", "https://www.bosch.de", "stripe.com", "https://www.ikea.com/sa/en/"],
  "maxPagesPerSite": 6,
  "checkMx": true,
  "includePersonalEmails": true,
  "onlyOwnDomainEmails": false
}
```

#### Output example (shortened)

```json
{
  "domain": "bosch.de",
  "url": "https://www.bosch.de/",
  "status": "ok",
  "companyName": "Bosch in Deutschland",
  "primaryEmail": "kontakt@bosch.de",
  "primaryPhone": "+4971140040990",
  "emails": [
    { "email": "kontakt@bosch.de", "type": "generic", "onDomain": true, "verdict": "valid", "hasMx": true, "source": "mailto",
      "foundOn": ["https://www.bosch.de/impressum/"] }
  ],
  "phones": [
    { "e164": "+4971140040990", "international": "+49 711 40040990", "countryCode": "DE", "type": "fixed_line", "label": "phone", "source": "text" }
  ],
  "socials": {
    "linkedin": "https://www.linkedin.com/company/bosch",
    "facebook": "https://www.facebook.com/BoschGlobal",
    "instagram": "https://www.instagram.com/boschglobal"
  },
  "address": null,
  "language": "de",
  "countryGuess": "DE",
  "contactPageUrl": "https://www.bosch.de/kontakt/",
  "summary": "Bosch in Deutschland · kontakt@bosch.de · +4971140040990 · 3 social profile(s)"
}
```

Sites that can't be read return a free row such as `{"status": "error", "errorCode": "BLOCKED", …}`. Other codes are `SITE_UNREACHABLE`, `HTTP_404` and `INVALID_URL`.

### Output fields

| Field | Meaning |
| --- | --- |
| `primaryEmail`, `primaryPhone` | The best contact, chosen for you |
| `emails[]` | `email`, `type` (role / personal / generic), `onDomain`, `verdict` (valid / invalid / risky / unknown), `hasMx`, `isFreeProvider`, `isDisposable`, `source`, `foundOn` |
| `phones[]` | `e164`, `international`, `national`, `countryCode`, `type`, `label` (phone / mobile / fax / whatsapp), `source`, `foundOn` |
| `socials`, `socialProfiles` | Best URL per network, and up to 5 per network |
| `companyName`, `legalName`, `description`, `logo`, `favicon` | Company identity |
| `address`, `geo`, `openingHours`, `vatId`, `foundingDate` | From structured data, when published |
| `contactPageUrl`, `contactFormUrls` | Where to reach them |
| `language`, `countryGuess`, `schemaTypes`, `pagesScanned` | Context |

### Pricing

**Pay per event: $0.003 per website profiled ($3 per 1,000).** You're charged once per site whose homepage loaded, however many pages were read. Unreachable, blocked and invalid sites cost nothing. The Actor never starts more sites than your **Max cost per run** can pay for.

### Use from AI agents (MCP) and code

- **MCP:** `https://mcp.apify.com?tools=actors,dodiabak/website-contact-extractor` works in Claude, Cursor, VS Code, n8n and others. Example prompt: *"Get the contact email, phone and LinkedIn page for these 20 domains."*
- **API:** `POST https://api.apify.com/v2/acts/dodiabak~website-contact-extractor/run-sync-get-dataset-items?token=YOUR_TOKEN`
- **Pairs well with:** [Contact Data Cleaner](https://apify.com/dodiabak/contact-data-cleaner) to normalize and dedupe your CRM, and Google Maps scrapers for domain lists.

### Responsible use

The Actor reads only information that companies publish on their own public websites. It doesn't log in, bypass bot protection, guess or brute-force addresses, or send any email. Personal addresses can be switched off with **Include personal emails = false**. When you store or contact people, follow the data-protection and anti-spam laws that apply to you (GDPR, CAN-SPAM, PECR…).

### Limitations

- Sites that render contacts only with JavaScript, or that block automated visitors (some big brands), may return fewer results or a `BLOCKED` row (free).
- The address comes from structured data (schema.org) only. Free-text addresses aren't guessed, to keep precision high.

# Changelog

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

# Actor input Schema

## `websites` (type: `array`):

Domains or URLs, one per line: acme.com, https://www.acme.de/, or even an email address (its domain is used). Duplicate domains are merged.

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

Homepage + the best contact, imprint (Impressum), about and team pages. 6 finds almost everything; raise for big sites.

## `checkMx` (type: `boolean`):

Checks that each email's domain can receive mail (DNS MX). Marks dead domains as invalid.

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

Keep addresses that look like a person's (first.last@). Turn off to return only role/generic addresses such as info@ or sales@.

## `onlyOwnDomainEmails` (type: `boolean`):

Drop addresses on other domains (agency, Gmail, partners).

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

ISO code (US, GB, DE, SA…) used for local numbers without + when the country can't be detected from the site's address, TLD or language.

## `respectRobotsTxt` (type: `boolean`):

Skip sub-pages that the site's robots.txt disallows. The homepage you entered is always read.

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

Timeout in seconds for each HTTP request made to a website.

## `concurrency` (type: `integer`):

Number of websites processed in parallel.

## `includeFailedSites` (type: `boolean`):

Error rows (status = error) are free and explain why a site failed (unreachable, blocked, HTTP error).

## `dedupeDomains` (type: `boolean`):

If enabled, duplicate domains in the input list are processed only once.

## Actor input object example

```json
{
  "websites": [
    "apify.com",
    "https://www.bosch.de",
    "stripe.com",
    "https://www.ikea.com/sa/en/"
  ],
  "maxPagesPerSite": 6,
  "checkMx": true,
  "includePersonalEmails": true,
  "onlyOwnDomainEmails": false,
  "respectRobotsTxt": true,
  "requestTimeoutSecs": 20,
  "concurrency": 10,
  "includeFailedSites": true,
  "dedupeDomains": true
}
```

# Actor output Schema

## `profiles` (type: `string`):

No description

## `leads` (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 = {
    "websites": [
        "apify.com",
        "https://www.bosch.de",
        "stripe.com",
        "https://www.ikea.com/sa/en/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dodiabak/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 = { "websites": [
        "apify.com",
        "https://www.bosch.de",
        "stripe.com",
        "https://www.ikea.com/sa/en/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("dodiabak/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 '{
  "websites": [
    "apify.com",
    "https://www.bosch.de",
    "stripe.com",
    "https://www.ikea.com/sa/en/"
  ]
}' |
apify call dodiabak/website-contact-extractor --silent --output-dataset

```

## MCP server setup

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