# 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
- **Features:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 domain 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

## 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: **from $1.50 per 1,000 websites ($1.20 on Gold and above)**. That
means no surprise bills from a runaway crawl on a big site, and **dead, parked and blocked domains are free**. In our October
test on 40 shared websites it found a company email on 75% against 43% for the most-used alternative, at about a sixth of the cost.

### Quick start

1. Click **Try for free** and paste websites, domains or email addresses into **Domains**, one per line (a comma-separated list on one line works too).
2. Press **Start**. A hundred sites take about a minute.
3. Open the **Output** tab, or download JSON, CSV or Excel. Every input gets a row with a `status`.

```json
{ "domains": ["stripe.com", "https://www.zunicafe.com", "info@tartinebakery.com"] }
```

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`) on the Free plan; $1.40 Bronze, $1.30 Silver, $1.20 Gold and above. 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`; $0.24 on Gold and above), only when `checkEmails` is on.

### Use it from n8n, Make, Zapier or code

- **n8n:** add the *Apify* node, operation **Run Actor and get dataset items**, actor `tidyharvest/website-contacts-scraper`, input `{ "domains": ["={{ $json.website }}"] }`. Each incoming item becomes one row.
- **Make:** the *Apify* module **Run an Actor**, then **Get Dataset Items**; map your sheet's website column into `domains`.
- **Google Sheets or a CRM export:** paste the column straight into **Domains**; the `input` field in each row is your original value, so you can join results back.
- **API:** one call returns the rows as JSON.

```bash
curl -X POST "https://api.apify.com/v2/acts/tidyharvest~website-contacts-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>"   -H "Content-Type: application/json"   -d '{ "domains": ["stripe.com", "zunicafe.com"] }'
```

### FAQ

**How do I get emails for a list of websites?** Paste them into **Domains** (or pass the dataset id of a Google Maps run in
`datasetIds`) and press Start. Each row has `emails` (company mailboxes first), `phones`, `socials`, `address` and a `status`.

**What counts as a company email?** Mailboxes such as info@, sales@, hello@ or support@, business-named free-mail boxes and, if
you leave `includePersonalEmails` on, staff addresses. Each email carries a `type` so you can filter.

**Why did a site come back `blocked` or `dead`?** `blocked` means the site's bot protection answered instead of the site;
`dead` means it did not respond (no DNS, refused or timed out). Both are free. Sites that drop our first connection are
retried once automatically.

**Do I pay twice for the same site?** No. Repeated domains in one run are read and charged once, and `skipDatasetIds` skips
domains from earlier runs.

**Can it read a chain's location page?** Yes: `rotorooter.com/columbus/` returns that branch's phone and email, not head office's.

**Is it legal to scrape emails from websites?** See *Is it legal?* above: it returns only what each site publishes itself.
Named-person emails are personal data under GDPR, so have a lawful basis before emailing them and turn `includePersonalEmails` off if unsure.

### 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.

### Related actors

Other actors from the same publisher that pair well with this one:

- [Google Maps Places Scraper](https://apify.com/tidyharvest/google-maps-places-scraper): Businesses, phones and websites from Google Maps.
- [Company Lookup](https://apify.com/tidyharvest/company-lookup): Company profile (website, ticker, LEI, HQ) from a name or domain.
- [YouTube Channel Contacts Scraper](https://apify.com/tidyharvest/youtube-channel-contacts-scraper): Emails, websites and socials for YouTube channels.
- [Y Combinator Companies Scraper](https://apify.com/tidyharvest/yc-companies-scraper): All Y Combinator companies with founders and open jobs.

### Feedback and requests

We welcome feedback: a field or option you wish this had, a change to how it works, or a scraper you would like us to build.
Open an issue in the **Issues** tab of this page and tell us what you need. We read every one, and we build the requests that
come up most. If this actor works well for you, a short review helps other people find it.

# 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/m2fYtVacpF9F4PVoh/openapi.json
