# Website Contacts Scraper: Emails, Phones, Socials (`tidyharvest/website-contacts-scraper`) Actor

Built to beat Contact Details Scraper: on the same 40 sites it found an email on 85% vs 68% and a company mailbox on 75% vs 43%, plus addresses, for 1/6 the cost: a flat $1.50 per 1,000 sites, not $2 per 1,000 pages (~$9.20 per 1,000 sites). Dead sites free.

- **URL**: https://apify.com/tidyharvest/website-contacts-scraper.md
- **Developed by:** [TidyHarvest](https://apify.com/tidyharvest) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 Contacts Scraper: Emails, Phones, Socials

Turn a list of **websites or domains** into **one clean row per company**: **emails**, **phone numbers** (E.164), **social media
links** (LinkedIn, Facebook, Instagram, X/Twitter, YouTube, TikTok and more), **postal address**, company name and a
**tech stack** guess, plus a clear **status** for every domain (ok, dead, parked, blocked).

You pay a **flat price per domain**, not per page crawled. That means no surprise bills from a runaway crawl on a big site, and
**dead, parked and blocked domains are free**.

It takes **Google Maps results directly**: paste the dataset id from a Maps scraper run and every place's website gets
enriched, with the place name and id carried along so you can join it back.

### Compared with Contact Details Scraper

We built this to beat [Contact Details Scraper](https://apify.com/vdrmota/contact-info-scraper)
(`vdrmota/contact-info-scraper`), the most-used contact scraper on Apify Store. In October 2026 we ran both on the
**same 40 websites** (small businesses from Google Maps in 8 countries plus B2B companies), theirs at up to 5 pages per site,
and checked 25 of the sites by hand.

| | This actor | Contact Details Scraper |
|---|---|---|
| Sites with an email found | **85%** | 68% |
| Sites with a company mailbox (info@, sales@...) | **75%** | 43% |
| Emails on the page that were found (25 hand-checked sites) | **93%** | 33% |
| Main phone found (hand-checked) | **95%** | 90% |
| Sites with an email or a phone | **92.5%** | 90% |
| Postal address | **52%** | none (no address field) |
| What the 40 sites cost | **$0.06** | $0.37 |

**Why the price looks different:** their Store card says $2.00 per 1,000 *pages*. A site is several pages (home,
contact, about, imprint), so in our test it worked out to about **$9.20 per 1,000 sites**. This actor charges
**$1.50 per 1,000 sites**, however many pages it reads, and nothing for dead, parked or blocked sites. Higher Apify plans
pay less per page there (about $4.80 per 1,000 sites at Gold), still about 3x this price.

**Where Contact Details Scraper still does more:** it routes every site through a proxy (it read all 40; we read 38),
can render JavaScript-only sites in a browser, covers a few more social networks, and sells add-ons we don't
(named-lead enrichment with job titles, social-profile enrichment, mailbox-level email verification).

### How well it works

Measured in October 2026 on **203 real websites**: small businesses drawn from Google Maps in 8 countries (US, UK,
Germany, France, Mexico, Australia, Canada, India: plumbers, dentists, accountants, salons, cafés, lawyers) plus 30
B2B/SaaS companies.

- **An email or phone for 87%** of sites (176 of 203). **A company email for 63%** (127 of 203), and **an email of any
  kind, staff included, for 75%** (107 of 142). Most sites without one only offer a contact form.
- **The phone Google Maps lists for the business was found on 75%** of sites (128 of 170).
- **A postal address for 54%**, read from schema.org data or the page text in each country's format.
- On 25 sites checked by hand: the **main phone was right on 95%**, and 93% of the emails the site shows were found.
- **142 sites in under 2 minutes**, for about $0.006 of platform compute.
- **Chain location pages** (`rotorooter.com/columbus/`) return that branch's phone and email, not head office's.

### What you can do with it

- **Enrich Google Maps leads:** run a Maps search, then pass its dataset id here to add emails, phones and socials to every business.
- **Clean up a CRM or lead list:** find which domains are dead or parked (free) and get current contact details for the rest.
- **Scheduled refreshes:** pass last run's dataset in `skipDatasetIds` and only new domains are processed and charged.
- **Find a company's social profiles** and the platform its website runs on (Shopify, WordPress, Wix, HubSpot...).

### Input

| Field | What it does |
|---|---|
| `domains` | Websites, domains or email addresses, one per line. A URL with a path (a chain's location page) is read first and is its own row. |
| `datasetIds` | Read websites from other Apify datasets (e.g. a Google Maps scraper run). |
| `skipDatasetIds` | Skip domains already processed in earlier runs of this actor. |
| `maxPagesPerDomain` | Homepage + best contact/imprint/about/team pages (default 5, max 20). Price is per domain regardless. |
| `timeoutSecsPerDomain` | Cut off slow sites after this many seconds (default 30). |
| `country` | Default country for local phone numbers and addresses (US, GB, DE...). Empty = the Maps row's country, else guess from the domain. |
| `retryBlockedWithProxy` | Retry sites that refuse cloud servers once through Apify Proxy (on by default; still free if they stay blocked). |
| `includePersonalEmails` | Also return staff emails (jane.doe@, jason@). On by default; off = company mailboxes only. |
| `checkEmails` | Paid add-on: syntax, MX, disposable, role and free-mail checks for each email. |
| `maxItems` | Stop after this many charged domains (caps your cost). |

Example:

```json
{ "domains": ["tartinebakery.com", "https://www.zunicafe.com"], "maxPagesPerDomain": 5 }
```

From a Google Maps run:

```json
{ "datasetIds": ["<your Maps run's dataset id>"] }
```

### Output

One item per domain:

```json
{
  "domain": "zunicafe.com",
  "status": "ok",
  "companyName": "Zuni Café",
  "emails": ["jobs@zunicafe.com"],
  "emailDetails": [{"email": "jobs@zunicafe.com", "type": "generic", "sameDomain": true, "foundOn": ["https://zunicafe.com/"]}],
  "phones": ["+14155522522"],
  "socials": {"facebook": "https://facebook.com/zunicafe", "instagram": "https://instagram.com/zunicafe"},
  "address": {"street": "1658 Market Street", "city": "San Francisco", "region": "CA", "postalCode": "94102", "country": "US", "source": "text"},
  "techStack": ["WordPress", "Google Analytics"],
  "url": "https://zunicafe.com/",
  "contactPages": ["https://zunicafe.com/contact/"],
  "pagesCrawled": 2,
  "hiddenEmails": {"personal": 0, "other": 0, "otherDomain": 0},
  "statusDetail": null
}
```

**Status values:** `ok` (site read, charged), `dead` (no DNS, connection refused, timeout), `parked` (for-sale or parking page),
`blocked` (bot protection answered instead of the site), `invalid` (not a domain, or a social/listing URL like a Google Maps link).
Only `ok` is charged.

**Which emails are returned:** company mailboxes such as info@, sales@, hello@, support@, press@, bookings@ (also kontakt@,
ventas@, accueil@ and other languages), and free-mail boxes named after the business (`room19cafe@gmail.com` for
room19.com.au, `type: "business"`), plus staff addresses: named people (jane.doe@, `type: "personal"`) and single-word boxes
that may be a first name (jason@, `type: "other"`). Company mailboxes are listed first. Set `includePersonalEmails` to false
to keep only company mailboxes; the others are then counted in `hiddenEmails`. Addresses on unrelated domains (the web agency, a
regulator listed in the imprint, a software vendor) are dropped too (`hiddenEmails.otherDomain`). Placeholders such as
max@mustermann.de are ignored. This keeps the output to the business's own people and mailboxes.

**Is it legal?** It returns only what the website itself publishes. Emails of named people are personal data under GDPR
(EU/UK) and similar laws: if you email them, you need a lawful basis (usually legitimate interest), and you must follow
anti-spam rules such as CAN-SPAM and PECR. If in doubt, turn `includePersonalEmails` off.

With `checkEmails` on, each row gets `emailChecks`: `syntaxValid`, `mxFound`, `mxHosts`, `disposable`, `role`, `freeMail` and a
`verdict` (`accepts-mail`, `risky`, `invalid`, `unknown`). No SMTP probe is sent, so `accepts-mail` means the domain receives
email, not that the exact mailbox exists.

### Pricing

- **$1.50 per 1,000 domains read** (`domain-processed`). Flat, however many pages it takes. Dead, parked, blocked and invalid domains are free.
- **Email check add-on:** $0.30 per 1,000 emails checked (`email-checked`), only when `checkEmails` is on.

### Limits and notes

- Reads plain HTML. Sites that only render contact details with JavaScript, or sit behind bot protection, come back with fewer fields or `blocked` (free).
- Addresses come from schema.org data on the site, or the page text in the site's country format (US, UK, Canada, Australia,
  Germany, Austria, Switzerland, France, Spain, Italy, Mexico). When a page lists several different addresses (imprints often
  do), none is guessed.
- Fax numbers, dates and placeholder numbers (123-456-7890) are not returned as phones.
- Public business information from each company's own website only. No logins.

# Actor input Schema

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

One per line: a domain (acme.com), a URL or an email address (uses its domain). A URL with a path, like a chain's location page (rotorooter.com/columbus/), is read first, so you get that branch's phone and email; each location is its own row. Duplicates are removed.

## `datasetIds` (type: `array`):

Dataset ids (or dataset URLs) from another run, for example a Google Maps scraper. Each item's website field (website, url, domain, homepage) is used, and the place's name, placeId, phone and address are copied into `source` so you can join the results back.

## `skipDatasetIds` (type: `array`):

For scheduled refreshes: dataset ids from earlier runs of this actor. Domains already processed there are skipped and not charged.

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

Homepage plus the best contact, imprint, about and team pages found in its links. The price is per domain either way; more pages finds more but takes longer.

## `timeoutSecsPerDomain` (type: `integer`):

Slow sites are cut off after this and return what was found so far.

## `country` (type: `string`):

Two-letter country code used to read local phone numbers without a + prefix (US, GB, DE...). Empty = guess from the domain ending, else US.

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

Also return addresses that belong to a named person (jane.doe@) or a single first name (jason@), typed `personal` / `other`. Turn off to keep only company mailboxes (info@, sales@); the rest are then counted in hiddenEmails. If you contact EU/UK people, you are responsible for a lawful basis under GDPR.

## `checkEmails` (type: `boolean`):

Run each email through syntax, mail-server (MX), disposable, role and free-mail checks. Charged per email checked. No SMTP probe is sent.

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

Some small-business hosts refuse cloud servers. Sites that answer with bot protection are retried once through Apify Proxy (datacenter). Still free if they stay blocked.

## `maxItems` (type: `integer`):

Stop after this many sites have been read (also caps your cost). Dead, parked and blocked domains don't count. 0 or empty = no limit.

## Actor input object example

```json
{
  "domains": [
    "tartinebakery.com",
    "sightglasscoffee.com",
    "zunicafe.com"
  ],
  "maxPagesPerDomain": 5,
  "timeoutSecsPerDomain": 30,
  "includePersonalEmails": true,
  "checkEmails": false,
  "retryBlockedWithProxy": true
}
```

# Actor output Schema

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

One item per domain: emails, phones, socials, address and status.

# 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 = {
    "domains": [
        "tartinebakery.com",
        "sightglasscoffee.com",
        "zunicafe.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidyharvest/website-contacts-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 = { "domains": [
        "tartinebakery.com",
        "sightglasscoffee.com",
        "zunicafe.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tidyharvest/website-contacts-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 '{
  "domains": [
    "tartinebakery.com",
    "sightglasscoffee.com",
    "zunicafe.com"
  ]
}' |
apify call tidyharvest/website-contacts-scraper --silent --output-dataset

```

## MCP server setup

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