# Website Contact Finder: Emails, Phones & Socials (`bjgomes/website-contact-finder`) Actor

Find emails, phone numbers and social profiles on business websites, from a list of domains or a Google Maps dataset. One row per domain, with the page each contact came from. Flat price per domain with contacts found; domains with none are free.

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

## Pricing

$3.00 / 1,000 domain 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.

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 Finder reads business websites and returns their emails, phone numbers and social profiles, one row per domain, with the page each one came from. You pay a flat $0.003 for each domain where it finds an email or a phone. Domains where it finds nothing are free.

### What it does

Give it a list of domains, or the dataset from a Google Maps scraper run. For each domain it reads the homepage and up to 5 likely contact pages: contact, impressum, about, team and legal pages, picked from the homepage's links. It pulls out every email address, every phone number and every social profile it finds, and tells you the page each one was on.

Each email is tagged `role` (info@, sales@, office@) or `personal` (jane@, john.smith@). Turn on **Only role emails** and the personal ones are left out of the results.

The same homepage visit also gives you a few website checks for free: HTTPS, whether http:// redirects to https://, status code, response time, title and meta description, mobile viewport, CMS, the footer's copyright year and whether there's a sitemap.

A site it can't read doesn't fail the run. A site that's down, blocks bots or turns out to be a parked domain gets a row that says so, and isn't charged.

Here's what five rows look like:

| domain | type | primaryEmail | primaryPhone | contactPageUrl |
|---|---|---|---|---|
| acmeplumbing.com | contacts | info@acmeplumbing.com | (614) 555-0142 | https://acmeplumbing.com/contact |
| bobsbakery.com | contacts | hello@bobsbakery.com | 503-555-0187 | https://bobsbakery.com/visit-us |
| smile-dental.com | contacts | (none) | +1 512-555-0119 | https://smile-dental.com/contact-us |
| riverside-cafe.com | no-contacts | (none) | (none) | (none) |
| oldsite.com | error | (blocked: HTTP 403) | | |

(Example data.)

### Where the websites come from

- **A list.** Paste domains or URLs, one per line: `acme.com`, `www.acme.com` or `https://acme.com/contact` all mean the same domain, which is searched once.
- **A Google Maps dataset.** Run any Google Maps scraper first, then pick its dataset in **Dataset from a Google Maps Actor**. This Actor reads each place's `website` and keeps the place's name in the `name` column, so you can match rows back. Places without a website are skipped and cost nothing. This Actor never reads Google Maps itself.

A link to a Facebook, Instagram or Yelp page in the website column isn't the business's own site, so it's reported and not searched.

### Who it's for

- **Agencies and freelancers.** Turn a list of local businesses into a list you can contact, with the website checks to start the conversation.
- **Google Maps lead lists.** Add emails to places that only came with a phone number and a website.
- **Sales teams.** Fill in the contact page and general inbox for accounts you already know.
- **Researchers.** Check which businesses in a list have a working website at all.

### Input

| Field | What it does | Default |
|---|---|---|
| Websites or domains | Domains or URLs, one per line. Duplicates are dropped. | none |
| Dataset from a Google Maps Actor | A dataset whose items have a `website` field. | none |
| Only role emails | Keep info@, sales@ and the like; leave out addresses that name a person. | off |
| Maximum domains | Stops after this many unique domains. Your hard ceiling on cost. | 100 |
| Contact pages per domain | Pages read after the homepage, 0 to 5. | 5 |
| Website field in the dataset | Where the website is in each dataset item. Dots reach into nested fields. | `website` |
| Domains in flight at once | Domains searched in parallel. Each domain still gets one request at a time. | 10 |
| Request timeout (seconds) | The longest one request may take. | 15 |

Give at least one website or a dataset. Input field names in the API: `websites`, `datasetId`, `onlyRoleEmails`, `maxDomains`, `maxContactPages`, `datasetField`, `maxConcurrency`, `requestTimeoutSecs`.

The form starts with 2 domains, which costs at most $0.006.

Example input:

```json
{
  "websites": ["acmeplumbing.com", "https://www.bobsbakery.com/"],
  "datasetId": "YOUR_GOOGLE_MAPS_DATASET_ID",
  "onlyRoleEmails": true,
  "maxDomains": 1000
}
```

### Output

One row per domain. Export it from the Console as CSV, JSON, Excel, XML or HTML, or read it through the Apify API. The **Contacts** view shows the contact columns; the **Website signals** view shows the checks.

| Field | What it holds |
|---|---|
| `type` | `contacts` (at least one email or phone; charged), `no-contacts` (read, nothing found; free) or `error` (not read; free). |
| `status` | `ok`, or why the site wasn't read: `blocked`, `unreachable`, `robots-disallowed`, `http-error`, `parked`, `not-a-website`, `invalid`, `stopped`. |
| `domain`, `input`, `name` | The domain searched, what you gave for it, and the place's name from a Google Maps dataset. |
| `url` | The homepage, after redirects. |
| `primaryEmail` | The best email for a first contact: one on the site's own domain, a role address first. |
| `primaryPhone` | The first phone number, preferring a clickable `tel:` link. |
| `emails` | Every email: `email`, `kind` (`role` or `personal`), `sameDomain` (on the website's own domain) and `sourceUrl` (the page it was on). |
| `phones` | Every phone number: `phone` as written, `digits`, `from` (`tel-link` or `page-text`) and `sourceUrl`. |
| `socials` | Every social profile: `network` (facebook, instagram, linkedin, x, youtube, tiktok, pinterest, threads, whatsapp), `url` and `sourceUrl`. Share buttons and single posts are left out. |
| `emailsText`, `phonesText`, `socialsText` | The same lists as one line each, for spreadsheets. |
| `contactPageUrl` | The site's contact page, if it has one. |
| `emailHidden` | `true` when the site hides an email from bots (Cloudflare's email protection, or "name (at) domain"). It isn't decoded; look it up by hand on the contact page. |
| `personalEmailsLeftOut` | With Only role emails on, how many personal addresses were left out. |
| `https`, `httpRedirectsToHttps` | Whether the homepage is served over HTTPS, and whether http:// sends visitors there. |
| `statusCode`, `responseTimeMs` | The homepage's HTTP status and how long it took to answer. |
| `hasTitle`, `title`, `hasMetaDescription`, `metaDescription` | The homepage's title and meta description, and whether each is there. |
| `hasViewport` | Whether the homepage has a mobile viewport tag. |
| `generator`, `cms` | The generator tag, and the CMS or site builder it or the page's files point to (WordPress, Wix, Squarespace, Shopify and others). |
| `copyrightYear` | The latest year in the copyright notice. An old year often means a site nobody updates. |
| `hasSitemap`, `sitemapUrl` | Whether the site has a sitemap (from robots.txt, or /sitemap.xml). |
| `pagesRead` | Every page read for this domain. |
| `note` | Anything worth knowing about a row that was read, such as a page cut off at 3 MB. |
| `error` | Why the site wasn't read, in words. Empty when it was. |

Example row (JSON):

```json
{
  "type": "contacts",
  "status": "ok",
  "domain": "acmeplumbing.com",
  "name": "Acme Plumbing",
  "url": "https://www.acmeplumbing.com/",
  "primaryEmail": "info@acmeplumbing.com",
  "primaryPhone": "(614) 555-0142",
  "contactPageUrl": "https://www.acmeplumbing.com/contact",
  "emails": [
    { "email": "info@acmeplumbing.com", "kind": "role", "sameDomain": true, "sourceUrl": "https://www.acmeplumbing.com/" },
    { "email": "jane@acmeplumbing.com", "kind": "personal", "sameDomain": true, "sourceUrl": "https://www.acmeplumbing.com/team" }
  ],
  "phones": [
    { "phone": "(614) 555-0142", "digits": "6145550142", "from": "tel-link", "sourceUrl": "https://www.acmeplumbing.com/" }
  ],
  "socials": [
    { "network": "facebook", "url": "https://www.facebook.com/acmeplumbing", "sourceUrl": "https://www.acmeplumbing.com/" }
  ],
  "https": true,
  "httpRedirectsToHttps": true,
  "statusCode": 200,
  "responseTimeMs": 640,
  "hasMetaDescription": false,
  "hasViewport": true,
  "cms": "WordPress",
  "copyrightYear": 2019,
  "hasSitemap": true,
  "error": null
}
```

Each signal is a first look. For the full check, run [Site Audit](https://apify.com/bjgomes/site-audit-seo) (every page's SEO issues, with fixes), [Broken Link Checker from Sitemap](https://apify.com/bjgomes/broken-link-checker-from-sitemap) or [Bulk PageSpeed Insights](https://apify.com/bjgomes/bulk-pagespeed-insights-for-a-sitemap) on the same site.

### Pricing

One price, per domain, with nothing on top.

- **Each domain where it finds at least one email or phone:** $0.003, so $3 per 1,000.
- **Free:** domains where it finds no email or phone, sites that are down, blocked, parked or disallowed by robots.txt, places without a website, duplicates, and anything it never got to.
- **No start fee, no per-page charge, no add-ons.** Reading 6 pages of a domain costs the same as reading 1. Social profiles and the website checks come with the row.

| Domains you give it | The most it can cost |
|---|---|
| 100 | $0.30 |
| 1,000 | $3.00 |
| 10,000 | $30.00 |

1,000 domains never cost more than $3. If only 600 of them have contacts, you pay $1.80.

Set Maximum domains, or a maximum cost per run, for a hard ceiling. A domain is only started when your maximum cost still covers it (as if it will find contacts), so the run never finds contacts it can't give you. When the next domain wouldn't fit, the run stops and its last row says how many domains weren't checked.

### How it handles the hard cases

- **Bot protection.** A site that answers 403, 401 or 429, or shows a CAPTCHA or "checking your browser" page, is reported as `blocked` and left alone. It isn't retried through proxies or a browser. Free.
- **Emails hidden from bots.** Cloudflare's email protection and "name (at) domain" are a site's way of saying no to email collectors, so they aren't decoded. The row says `emailHidden: true`.
- **Text that only looks like an email.** Some sites scramble their text against copying and unscramble it with JavaScript, which turns into strings like `ntr@icspspo.eos`. An address is kept only if its top-level domain exists, and, when it's on another domain than the site's, if that domain has a DNS record. File names like `logo@2x.png` and placeholders like `you@example.com` are left out too.
- **Sites that don't answer.** It tries https, then http, then the www. host if the bare domain doesn't resolve. A site that still doesn't answer is `unreachable`, with the reason: no DNS record, connection refused, timed out or a TLS error. Free.
- **Parked and for-sale domains.** Recognised by the parking service they redirect to, or by "this domain is for sale" on the page. Reported as `parked`, so you don't pay for a domain marketplace's contact details. Free.
- **A Facebook page in the website column.** Reported as `not-a-website` and not searched.
- **Single-page sites and soft 404s.** When the homepage links to no contact page, it tries /contact and /contact-us. A page that turns out to be the homepage again, or a "not found" page answered with status 200, isn't counted.
- **JavaScript-only sites.** It reads the HTML the server sends, without a browser. Contact details that only appear after JavaScript runs (some Wix, GoDaddy and Squarespace templates) aren't seen. Those rows come back as `no-contacts` and aren't charged.
- **Big pages.** It reads up to 3 MB of HTML per page, and notes it when a page is longer.
- **Time limit.** Near the run's timeout it stops starting domains, finishes the ones in flight and ends normally, with a row saying how many weren't checked.

### robots.txt and good manners

- It respects robots.txt (RFC 9309). If robots.txt disallows the homepage, or can't be read because the server errors, nothing on that site is fetched and the row says why.
- It honours Crawl-delay up to 10 seconds. A site that asks for more gets one page read, the homepage.
- It sends one request at a time to a site, and reads at most 6 pages of it.
- It identifies itself honestly as `WebsiteContactFinder/0.1`. No proxies, no browser, no logins.
- Addresses on private networks, loopback and cloud metadata ranges are refused, including when a public site redirects to one.

### Using the contacts lawfully

This Actor finds contact details that businesses publish on their own websites. What you do with them is up to you, and so is following the law where you and your recipients are. This isn't legal advice; check with counsel for your case.

- **GDPR (EU and UK).** An email that names a person, such as jane@acme.de, is personal data even when it's published on a company website. You're the controller for what you collect, so you need a lawful basis and must tell people where you got their data. Under Apify's terms, the user is the controller and the Actor's developer is a processor for personal data in inputs and outputs. To keep personal data out entirely, turn on **Only role emails**.
- **Nothing is kept.** Each run's results go to your own dataset and nowhere else. The Actor keeps no database, caches nothing between runs and never shares one buyer's results with another.
- **CAN-SPAM (US).** B2B cold email is legal without prior consent if each message has a real sender, a subject line that isn't misleading, a physical postal address and a working way to opt out, and opt-outs are honoured within 10 business days. Penalties can exceed $50,000 per email that breaks the rules. See the [FTC's compliance guide](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business).
- **France** allows B2B email to professional addresses on an opt-out basis, if the message relates to the recipient's job.
- **Germany** is stricter: B2B email generally needs prior consent (UWG §7).

### Compared with other contact finders

Other contact scrapers in the Store charge per page crawled, per proxy request or per add-on, so the bill depends on how big each site is and which boxes were ticked. Users report paying $71 for 1,105 rows because a run crawled 18,011 pages, and $154 for 51 places once enrichment add-ons were counted.

This Actor charges once per domain where it finds an email or phone, and the price doesn't change with how many pages it reads. It doesn't use a browser or residential proxies, so it's cheaper to run, and it doesn't work around bot protection. The trade-off is that it misses contacts that only appear after JavaScript runs.

Not made by or affiliated with Google, Apify's Google Maps Actors or any social network.

### FAQ

**How do I find emails for a list of websites?**
Paste the domains into **Websites or domains** and run it. You get one row per domain with its emails, phones and social profiles. Export it as CSV.

**How do I add emails to my Google Maps results?**
Run your Google Maps scraper, then pick its dataset in **Dataset from a Google Maps Actor**. Each row keeps the place's name, so you can join the two.

**How often does it find something?**
On a sample of 20 small-business websites (plumbers, bakeries, dentists and carpenters in the US and Germany, October 2026), 3 refused it with a 403. Of the 17 it could read, it found an email or phone on 16 (94%), an email on 14 (82%), a phone on 16 and the contact page on 15. Across all 20, that's contacts for 16 (80%). It read about 4 pages per site. Sites whose contact details only appear after JavaScript runs find less, and rows with nothing found aren't charged.

**What's the difference between role and personal emails?**
A role address belongs to a function (info@, sales@, office@, bookings@). A personal one names someone (jane@, j.smith@). Anything it can't tell is treated as personal, the cautious choice.

**Why is a site `blocked`?**
The site refused the request or showed a bot check. This Actor doesn't get around that, by design. Visit the site yourself to find its contact details.

**Why are there emails on other domains?**
Many small businesses use a Gmail or Outlook address, and some sites show their web designer's address in the footer. `sameDomain` tells them apart.

**Does it verify the emails?**
No. It reports what the site publishes. An address on a site can still bounce.

**Can I run it on a schedule?**
Yes, but each run searches the sites again and charges again for domains with contacts. It doesn't keep results between runs.

**Can I call it from my own code?**
Yes. Start it and read the results through the Apify API or the Python and JavaScript clients. It also works with webhooks, Make, Zapier and Google Sheets.

### Limits

- Plain HTTP, no browser: contact details added by JavaScript aren't seen.
- At most 6 pages per domain: the homepage and 5 contact pages.
- Sites behind bot protection, a login or a robots.txt that disallows it aren't read.
- Emails hidden from bots aren't decoded.
- Phone numbers are reported as written. Numbers without a country code aren't converted to international format.
- It runs in 256 MB by default.

### Integrations

This Actor runs on Apify, so everything the platform does works with it.

- **API.** Start a run and read the results from your own code, in any language. [Apify API](https://docs.apify.com/integrations/api)
- **Schedules.** Run it again when your list changes. [Schedules](https://docs.apify.com/actors/running/schedules)
- **Tasks.** Save your input once as a task and run it again with one click. [Tasks](https://docs.apify.com/actors/running/tasks)
- **Webhooks.** Get a call to your own URL when a run succeeds or fails. [Webhooks](https://docs.apify.com/integrations/webhooks)
- **Google Sheets and Drive.** Download the results as CSV or Excel and open them in Sheets, or add a Google Sheets step after this Actor in Make or Zapier. [Datasets and exports](https://docs.apify.com/storage/dataset), [Google Drive](https://docs.apify.com/integrations/drive)
- **Zapier, Make and n8n.** Start a run from another app and send the results on to your CRM. [Zapier](https://docs.apify.com/integrations/zapier), [Make](https://docs.apify.com/integrations/make), [n8n](https://docs.apify.com/integrations/n8n)
- **AI agents (MCP).** Claude, ChatGPT and other MCP clients can find and run this Actor through the Apify MCP server. [Apify MCP server](https://docs.apify.com/integrations/mcp)
- **Other Actors.** Feed it the dataset of a Google Maps scraper when that run finishes. [Actor-to-Actor](https://docs.apify.com/integrations/actors)

### Works with

- [Site Audit](https://apify.com/bjgomes/site-audit-seo): an SEO score from 0 to 100 for every page, with a fix for each issue.
- [Bulk PageSpeed Insights for a Sitemap](https://apify.com/bjgomes/bulk-pagespeed-insights-for-a-sitemap): performance scores and Core Web Vitals for every page, mobile and desktop.
- [Broken Link Checker from Sitemap](https://apify.com/bjgomes/broken-link-checker-from-sitemap): every broken link on every page in a sitemap, with the page it's on.
- [Sitemap URL Extractor & Status Checker](https://apify.com/bjgomes/sitemap-url-extractor-status-checker): the full URL list of a site, with lastmod and status codes.
- [Website Screenshot API](https://apify.com/bjgomes/website-screenshot): a clean full-page screenshot of every URL, with cookie banners and pop-ups hidden.

### Support

Found a bug or need a feature? Open an issue on this Actor's **Issues** tab. Include the run ID and the domain it got wrong.

# Actor input Schema

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

Business websites to search, one per line: acme.com, www.acme.com or a full URL such as https://acme.com/contact. Each domain is searched once, however many times it's listed.

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

A dataset from a Google Maps scraper run (or any dataset with a website column). The Actor reads each item's website, plus the place's name from 'title' or 'name'. Places without a website are skipped and not charged.

## `onlyRoleEmails` (type: `boolean`):

Keep only addresses that belong to a role, such as info@, sales@ or office@, and leave out addresses that name a person (jane@). A domain whose only contacts were personal emails then counts as one without contacts, and isn't charged.

## `maxDomains` (type: `integer`):

Stop after this many unique domains across both inputs. A hard ceiling on what a run can cost: at $0.003 per domain with contacts, 1,000 domains never cost more than $3.

## `maxContactPages` (type: `integer`):

Besides the homepage, read at most this many likely contact pages per domain: contact, impressum, about, team and legal pages, in that order. Lower is faster.

## `datasetField` (type: `string`):

The dataset field that holds the website. Google Maps Actors use 'website'. A nested field can be named with dots, such as 'contact.website'.

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

Domains searched in parallel. Each domain gets one request at a time, whatever this is set to.

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

The longest one request may take, connecting and reading included. A site that doesn't answer in time is reported as unreachable.

## Actor input object example

```json
{
  "websites": [
    "apify.com",
    "wikimediafoundation.org"
  ],
  "onlyRoleEmails": false,
  "maxDomains": 2,
  "maxContactPages": 5,
  "datasetField": "website",
  "maxConcurrency": 10,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

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

One row per domain: emails, phones and social profiles with the page each came from, or why the site wasn't read.

## `signals` (type: `string`):

The same rows' free website checks: HTTPS, status, response time, title, meta description, mobile viewport, CMS, copyright year and sitemap.

# 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",
        "wikimediafoundation.org"
    ],
    "maxDomains": 2
};

// Run the Actor and wait for it to finish
const run = await client.actor("bjgomes/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",
        "wikimediafoundation.org",
    ],
    "maxDomains": 2,
}

# Run the Actor and wait for it to finish
run = client.actor("bjgomes/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",
    "wikimediafoundation.org"
  ],
  "maxDomains": 2
}' |
apify call bjgomes/website-contact-finder --silent --output-dataset

```

## MCP server setup

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