# Contact Details Scraper: Google Maps Leads to Emails & Phones (`jfaro19/website-contact-finder`) Actor

Turn Google Maps and directory lead lists into emails, phones and social profiles. Paste websites or a Google Maps dataset; it crawls each site's contact pages, renders JavaScript sites, and can verify emails. Pay only when a contact is found.

- **URL**: https://apify.com/jfaro19/website-contact-finder.md
- **Developed by:** [Jason Faro](https://apify.com/jfaro19) (community)
- **Categories:** Lead generation, Automation
- **Features:** Limited permissions
- **Stats:** 23 total users, 4 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 website with contacts

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 Contact Details Scraper do?

**Contact Details Scraper turns Google Maps and directory lead lists into emails, phone numbers and social profiles.** Give it a list of business websites, or point it at the results of a Google Maps scraper, and for every business it returns the **best email, phone number, social media profiles, address and contact page**, ready to export.

**You pay only for results: a flat price per website where at least one email or phone number is found.** Websites with no contacts, unreachable sites and errors are free, and crawling more pages never raises the price. Many contact scrapers charge for every page they load, whether or not they find anything.

For each website it crawls the homepage plus the pages most likely to hold contact details (Contact, About, Team, Locations, Impressum…). It **renders JavaScript-built sites in a real browser** when needed and can **verify emails** before you send. You can download results as CSV, Excel or JSON, or connect them to Google Sheets, n8n, Make, Zapier or your CRM.

![Example output: best email, phone, social profiles and email provider for each website](https://api.apify.com/v2/key-value-stores/lkZdY1T0fkViTlEcL/records/contact-output-example.png?signature=yUgNGH9BS8sU4w0EBd9v)

### Get emails for every roofer in Texas in 10 minutes

A step-by-step tutorial for turning Google Maps listings into an email list. The same steps work for any trade and any area.

1. **Scrape Google Maps.** Open [Google Maps Scraper](https://apify.com/compass/crawler-google-places) and search for `roofing contractor` in `Texas, USA`. Set the number of places, for example 300 to start, and under **website** keep only places **with a website**. Click **Start**.
2. **Find the contacts.** When it finishes, open **Contact Details Scraper**, and in **Dataset** pick the Google Maps run's dataset. Leave **Website field** as `website`. Click **Start**. Every result keeps the business's name, category, address, Maps phone, rating and Maps URL under `source`.
3. **Verify the emails (optional).** Turn on **Verify emails** to check each business's best email with MillionVerifier before you send anything.
4. **Filter to clean, sendable emails.** Open the **Leads** view in the **Output** tab, export to CSV or Excel, and keep rows where:
   - `emails.domainAcceptsMail` is `true`: the email's domain has a mail server.
   - `emails.sameDomain` is `true`: the address is on the company's own domain, not a third-party one.
   - `primaryEmailVerification` is `valid`, if you turned on verification.
5. **Export.** Download as CSV or Excel, or send the dataset straight to Google Sheets with the n8n templates below.

About 300 businesses take a few minutes per step. You pay the Google Maps scraper for the places, and this Actor only for websites where it found an email or phone number.

### Why use this contact details scraper?

- **Built for lead lists.** It reads Google Maps, Yelp and directory datasets directly and keeps the listing details next to the contacts. You don't have to merge spreadsheets.
- **Finds emails other tools miss.**
  - Decodes Cloudflare-protected emails and `name [at] domain [dot] com` style addresses.
  - Reads schema.org business data hidden in the page.
  - Renders JavaScript-built sites (React, Vue, Angular, single-page apps) in a headless browser when a plain request sees an empty page.
- **Cleaner data.**
  - Placeholders (`you@example.com`), image names (`logo@2x.png`) and tracking IDs are removed.
  - Phone numbers are validated and formatted in international E.164 format (`+15025610375`).
- **Knows which emails are worth using.** Each email is labeled:
  - `generic` (info@, sales@) or `personal`
  - on the company's own domain or not
  - from a free provider (Gmail, Yahoo…) or not
  - whether its domain can **receive mail** (checked via MX records), which catches typos like `info@compnay.com`
  - optionally, **verified** with MillionVerifier: valid, invalid, disposable, catch-all or unknown
- **Picks the best contact.** `primaryEmail` and `primaryPhone` give you one ready-to-use value per company.
- **Tells you why a site came back empty.** Every website without contacts gets a `noContactReason`, such as `blocked`, `js_rendered`, `contact_form_only` or `no_contacts_published`.

### Head-to-head test: same 61 websites

We ran this Actor and a popular per-page-priced contact scraper on the **same 61 real small-business websites** (roofers, plumbers, HVAC, landscapers), giving both the **same budget of 5 pages per site**:

| | This Actor | Per-page scraper |
|---|---|---|
| Cost for 61 websites | **$0.18** | $0.58 |
| Websites with an email | **37** | 32 |
| Websites with a phone number | **58** | 54 |
| Phone output | Validated, one format, no duplicates | Mixed formats and duplicates, plus "uncertain" numbers that included map coordinates and IDs |

Every email and phone number this Actor returned in that run was re-checked on the page it came from: **41/41 emails and 58/58 phones** were correct. *(Test date: October 2026. Results on your own list will vary.)*

### How to scrape emails and phone numbers from a list of websites

1. Click **Try for free**.
2. Paste websites into the **Websites** field, one per line. `example.com`, `https://www.example.com/contact` and `jane@example.com` all work.
3. Optionally, adjust **Max pages per website** (default 5), **Default country** for phone numbers, and **Verify emails**.
4. Click **Start**.
5. Download the dataset from the **Output** tab, or connect it to your tools.

### Complete lead enrichment workflow

Chain three Actors to get a full profile of every company on your list:

1. **Contact Details Scraper** (this Actor): emails, phones, socials, address.
2. **[Domain Intelligence](https://apify.com/jfaro19/domain-intel)**: tech stack, SaaS tools, email provider and SPF/DKIM/DMARC grade. Useful for segmenting by technology.
3. **[Website Screenshot & PDF](https://apify.com/jfaro19/website-screenshot)**: a clean screenshot of each homepage (no cookie banners) for personalized outreach or website audits.

Example with the [Apify Python client](https://docs.apify.com/api/client/python):

```python
from apify_client import ApifyClient

client = ApifyClient('<YOUR_API_TOKEN>')
maps_dataset_id = '<GOOGLE_MAPS_RUN_DATASET_ID>'

contacts = client.actor('jfaro19/website-contact-finder').call(run_input={'datasetId': maps_dataset_id})
leads = list(client.dataset(contacts['defaultDatasetId']).iterate_items())
websites = [lead['domain'] for lead in leads if lead.get('contactsFound')]

tech = client.actor('jfaro19/domain-intel').call(run_input={'domains': websites})
shots = client.actor('jfaro19/website-screenshot').call(
    run_input={'urls': [f'https://{d}' for d in websites]})
```

Join the three datasets on the domain.

### Use it in n8n

Two free, ready-made n8n workflows run this Actor and save the results to Google Sheets:

- [Find local business leads on Google Maps and save to Google Sheets](https://n8n.io/workflows/20404-find-local-business-leads-on-google-maps-with-apify-and-save-to-google-sheets/): search Google Maps, find each business's email, phone and socials, and get one clean row per lead.
- [Enrich company websites from Google Sheets with emails and phones](https://n8n.io/workflows/20502-enrich-company-websites-from-google-sheets-with-emails-and-phones-using-apify/): read a column of websites from your sheet and write the contacts back.

Import the template, connect your Apify and Google accounts, and run it. Each workflow has a spend cap you can change.

### Input

| Field | Type | Description |
|---|---|---|
| `websites` | array of strings | Domains, URLs or emails. They are normalized and de-duplicated. |
| `datasetId` | string | Optional. Read websites from an Apify dataset, such as a Google Maps scraper's output. |
| `websiteField` | string (default `website`) | Field in that dataset that holds the website URL. |
| `maxPagesPerWebsite` | integer (default `5`) | Homepage plus the most likely contact pages. Price doesn't depend on this. |
| `defaultCountry` | string (default `US`) | Country used for phone numbers written without a country code. Country-code domains like `.de` and `.co.uk` are detected automatically. |
| `retryBlockedWithProxy` | boolean (default `true`) | Retry sites that block cloud servers through Apify Proxy. Proxy use is included in the price. |
| `browserFallback` | boolean (default `true`) | Render JavaScript-built homepages in a headless browser when a plain request finds no contacts. Included in the price. |
| `maxBrowserPagesPerWebsite` | integer (default `2`) | Pages the browser fallback opens per JavaScript-built site. |
| `verifyEmails` | boolean (default `false`) | Verify the best email of each website with MillionVerifier (paid add-on, see Pricing). |
| `verifyAllEmails` | boolean (default `false`) | Verify every email found, not just the best one. |
| `millionVerifierApiKey` | string (secret) | Optional. Your own MillionVerifier key: verification is then not charged by this Actor. |
| `maxConcurrency` | integer (default `15`) | Number of websites processed in parallel. |

```json
{
  "datasetId": "<GOOGLE_MAPS_DATASET_ID>",
  "maxPagesPerWebsite": 5,
  "verifyEmails": true
}
```

### Output

Each website produces one item. This example is from a real run on `apify.com`; the `emailVerification` values are illustrative:

```json
{
  "domain": "apify.com",
  "status": "ok",
  "contactsFound": true,
  "companyName": "Apify",
  "primaryEmail": "hello@apify.com",
  "primaryEmailVerification": "valid",
  "primaryPhone": null,
  "emailCount": 1,
  "emails": [
    {
      "email": "hello@apify.com",
      "type": "generic",
      "sameDomain": true,
      "freeMailProvider": false,
      "domainAcceptsMail": true,
      "foundOn": ["https://apify.com/contact"],
      "emailVerification": {
        "result": "valid",
        "subResult": "ok",
        "isCatchAll": false,
        "isDisposable": false,
        "isRoleBased": true,
        "provider": "millionverifier",
        "checkedAt": "2026-10-10T15:09:18+00:00"
      }
    }
  ],
  "phones": [],
  "socialProfiles": {
    "linkedin": "http://linkedin.com/company/apify",
    "x": "https://x.com/apify",
    "tiktok": "https://www.tiktok.com/@apifytech",
    "github": "https://github.com/apify"
  },
  "address": {
    "streetAddress": "Na Příkopě 959/27",
    "addressLocality": "Prague",
    "postalCode": "11000",
    "addressCountry": "CZ"
  },
  "contactPageUrl": "https://apify.com/contact",
  "hasContactForm": true,
  "contactFormUrl": "https://apify.com/contact-sales",
  "mailboxProvider": "Google Workspace",
  "websiteUrl": "https://apify.com/",
  "fetchedViaProxy": false,
  "renderedWithBrowser": false,
  "pagesCrawled": ["https://apify.com/", "https://apify.com/contact", "https://apify.com/contact-sales", "https://apify.com/about"]
}
```

*`emailVerification` and `primaryEmailVerification` appear only when **Verify emails** is on.*

#### Main fields

| Field | Description |
|---|---|
| `primaryEmail` / `primaryPhone` | The best single contact: same-domain generic inbox first, then other emails. Phones are ranked by how often they appear. |
| `emails[]` | Every email found, with `type`, `sameDomain`, `freeMailProvider`, `domainAcceptsMail`, the pages it was found on and, when verified, `emailVerification` |
| `emailVerification` | `result` (`valid`, `invalid`, `disposable`, `catch_all`, `unknown` or `error`), `subResult`, `isCatchAll`, `isDisposable`, `isRoleBased`, `provider`, `checkedAt` |
| `primaryEmailVerification` | Verification result of `primaryEmail`, as one column for exports |
| `phones[]` | Validated phone numbers in E.164 format, with the pages they were found on |
| `socialProfiles` | Facebook, Instagram, LinkedIn, X/Twitter, YouTube, TikTok, Pinterest, Threads, Telegram, WhatsApp, Discord, Snapchat, Reddit, GitHub, Yelp, Linktree |
| `allEmails`, `allPhones`, `facebook`, `instagram`, `linkedin`, `x`, `youtube`, `tiktok` | Flat copies for clean CSV/Excel exports (one column each) |
| `source` | When you use a dataset ID: the business's name, category, address, phone, rating and Maps URL from that dataset |
| `address` | Postal address from the site's structured data, when published |
| `contactPageUrl`, `contactFormUrl` | Where to reach the company if there's no email |
| `mailboxProvider` | The company's email provider (Google Workspace, Microsoft 365, Zoho, GoDaddy…) |
| `renderedWithBrowser` | `true` when the site was read with the headless-browser fallback |
| `noContactReason` | Only on websites without contacts: `js_rendered`, `blocked`, `timeout`, `unreachable`, `redirect_offsite`, `contact_form_only`, `no_contacts_published` or `error` |
| `status` | `ok`, `website_unreachable` or `error` (never charged) |

Each run also saves a **Run summary** (`RUN_SUMMARY` in the key-value store): counts by status and by `noContactReason`, browser fallback use, verification results and charged events. It contains no personal data.

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

The Actor uses **pay-per-event** pricing. You're charged for these events:

| Event | When it's charged |
|---|---|
| **Website with contacts** | Once per website where at least one email or phone number was found. Sites with no contacts, sites with only social profiles, unreachable sites and errors are free. Crawling more pages, proxy retries and browser rendering don't change the price. |
| **Email verified** | Only when **Verify emails** is on. Once per unique email that gets a decisive answer: `valid`, `invalid` or `disposable`. `catch_all`, `unknown` and failed checks are free, and each address is verified at most once per run. If you use your own MillionVerifier key, this event is never charged. |

See the **Pricing** tab for current prices. Set a **maximum cost per run** and the Actor stays within it. When the limit is close, it stops verifying emails first and keeps finding contacts as long as the budget allows.

### Tips

- Feed it the `website` field from Google Maps, Yelp or directory scrapes to turn business listings into contactable leads.
- For the cleanest outreach lists, filter on `emails.domainAcceptsMail = true`, `emails.sameDomain = true` and, if you verify, `primaryEmailVerification = valid`.
- If you only need the main contact, use `primaryEmail` and `primaryPhone`, or the **Leads** view.
- Check `noContactReason` and the Run summary to see why some sites came back empty.

### FAQ, disclaimers and support

**Is it legal?** The Actor only reads public web pages, the same way a visitor's browser would. It doesn't log in, submit forms or bypass access controls. Contact details can include personal data, for example a named employee's email. You're responsible for having a lawful basis to process it and for following anti-spam laws such as GDPR, CAN-SPAM and CASL when you contact anyone.

**Why didn't it find an email for some sites?** Many small businesses publish only a phone number or a contact form. The `noContactReason` field tells you which. You still get `contactFormUrl` when there's a form, and if no email or phone was found at all, you aren't charged.

**What does email verification send to MillionVerifier?** Only the email address being checked. MillionVerifier's own terms and privacy policy apply to that check.

**Did it save you time?** A quick review on the **Reviews** tab helps other people find it.

**Found a bug or a site it handles badly?** Open an issue on the **Issues** tab and include the website. Fixes ship regularly.

# Changelog

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

# Actor input Schema

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

Websites to search for contacts. Paste domains (example.com), URLs (https://www.example.com/contact) or email addresses; they are normalized and de-duplicated. Leave empty if you use a dataset ID below.

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

Pick an Apify dataset to read websites from, for example the output of a Google Maps scraper run, or paste its ID. Business name, category, address, phone, rating and Maps URL are carried into each result under "source". The Actor only gets read access to the dataset you select.

## `websiteField` (type: `string`):

Name of the field holding the website URL in the dataset items. Google Maps scrapers use "website".

## `maxPagesPerWebsite` (type: `integer`):

Homepage plus the most likely contact pages (Contact, About, Team, Impressum, Locations...). You pay per website, not per page, so this only affects speed.

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

Two-letter country code used to interpret phone numbers written without a country code (e.g. US, GB, DE). Country-code domains (.de, .co.uk, ...) are detected automatically.

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

Some websites block cloud servers. When a site refuses the normal request, retry it through Apify Proxy. Included in the price: you are never charged extra for proxy use.

## `browserFallback` (type: `boolean`):

Some websites (React, Vue, Angular, Wix-style builders) only show their content after JavaScript runs, so a plain request sees an empty page. When a homepage looks like that and no contacts were found, it is opened in a headless browser. Included in the price: you are never charged extra for browser rendering.

## `maxBrowserPagesPerWebsite` (type: `integer`):

How many pages (homepage plus the likeliest contact pages) the browser fallback opens per JavaScript-built website.

## `verifyEmails` (type: `boolean`):

Check whether the best email of each website can receive mail, using MillionVerifier. Adds an "emailVerification" object to each verified email. You pay per verified email only for a decisive answer (valid, invalid or disposable); catch-all, unknown and failed checks are free. See the Pricing tab. With your own MillionVerifier key below, there is no verification charge.

## `verifyAllEmails` (type: `boolean`):

Verify all emails found on each website instead of just the best one. Each address is verified (and charged) at most once per run.

## `millionVerifierApiKey` (type: `string`):

Use your own MillionVerifier account for verification. Verification is then not charged by this Actor (MillionVerifier bills your account). Stored encrypted.

## `maxConcurrency` (type: `integer`):

How many websites to process in parallel.

## Actor input object example

```json
{
  "websites": [
    "apify.com",
    "louisvilleroofing.com"
  ],
  "websiteField": "website",
  "maxPagesPerWebsite": 5,
  "defaultCountry": "US",
  "retryBlockedWithProxy": true,
  "browserFallback": true,
  "maxBrowserPagesPerWebsite": 2,
  "verifyEmails": false,
  "verifyAllEmails": false,
  "maxConcurrency": 15
}
```

# Actor output Schema

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

No description

## `runSummary` (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",
        "louisvilleroofing.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jfaro19/website-contact-finder").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",
        "louisvilleroofing.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("jfaro19/website-contact-finder").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",
    "louisvilleroofing.com"
  ]
}' |
apify call jfaro19/website-contact-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jfaro19/website-contact-finder"
        }
    }
}
```

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/jipdiW9Rwbp1Lruzx/builds/3o3UDLXKSjfjb0QOC/openapi.json
