# Contact Details Scraper: Emails & Phones for Google Maps Leads (`stellalytics/business-contact-finder`) Actor

Contact details scraper and email extractor for company websites or Google Maps leads: role emails (info@, sales@), company phones, social media pages, contact page and contact form. One row per company. Skips name-based emails like jane.doe@. Obeys robots.txt. No API key.

- **URL**: https://apify.com/stellalytics/business-contact-finder.md
- **Developed by:** [Stellalytics](https://apify.com/stellalytics) (community)
- **Categories:** Lead generation, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 company with contacts founds

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

## Contact Details Scraper: Emails & Phones for Google Maps Leads

**Business Contact Finder is a contact details scraper and email extractor for company websites and Google Maps leads. It finds how to contact a company from its website: company email addresses (info@, sales@, support@), phone numbers, social media pages, the contact page and the contact form.** Paste a list of websites, or pick a Google Maps scraper's results, and get **one clean row per company**, not one row per page.

Made for lead generation and sales teams, agencies and anyone enriching a list of local businesses or company domains. No API key. No login.

**Company contact points only.** The Actor keeps an email address only when it is a role mailbox (info@, sales@, support@, bookings@, hello@...) on the company's own domain. Addresses that look like a person's (jane.doe@, jdoe@, info.jane.doe@) or that sit on other domains (gmail.com, an agency's domain) are never output; the row only counts how many were skipped. Personal LinkedIn profiles are skipped, and a schema.org `Person`'s email or phone is never read.

### What you get for each company

| Field | What it is |
| --- | --- |
| `domain`, `status` | The company's domain, and `live`, `domain_not_found`, `unreachable`, `http_error`, `robots_disallowed`, `blocked_by_bot_protection`, `parked_domain` (the domain is for sale, parked or suspended) or `no_email` (only with **Only companies with an email**) |
| `companyName` | From the site's structured data (schema.org), its `og:site_name`, or the page title |
| `primaryEmail`, `emails` | Company role addresses found on the site (mailto links, page text, structured data), best first |
| `emailDomainAcceptsMail` | `true` when the email's domain has mail servers in DNS (MX records), `false` when it has none or doesn't exist, `null` when DNS didn't answer. A DNS check only: no mail server is contacted, so it can't prove a single mailbox exists |
| `phones` | Company numbers from `tel:` links and structured data, digits only (with `+` when the site gives an international number) |
| `socialLinks` | The social pages the site links to: LinkedIn company page, Facebook, Instagram, X, YouTube, TikTok, GitHub, Pinterest, Threads and Bluesky |
| `contactPageUrl`, `contactFormUrl` | The contact page, and the contact page that has a form |
| `otherEmailsSkipped` | How many other addresses were seen and left out (name-based or on other domains) |
| `pagesChecked`, `finalUrl`, `checkedAt` | How many pages were read, where the homepage redirected, when |
| `source` | Only with **Copy fields from the dataset**: the fields you chose from your input dataset (for example the Google Maps `title` and `placeId`), so each row matches back to your lead |

A `SUMMARY` record adds totals: websites with contacts, with an email, with a phone, with a contact form, and status counts.

#### Example row (apify.com, checked 2026-11-12)

```json
{
  "input": "apify.com",
  "domain": "apify.com",
  "status": "live",
  "companyName": "Apify",
  "primaryEmail": "hello@apify.com",
  "emailDomainAcceptsMail": true,
  "emails": ["support@apify.com", "hello@apify.com"],
  "phones": [],
  "socialLinks": {
    "linkedin": "http://linkedin.com/company/apify",
    "github": "https://github.com/apify",
    "tiktok": "https://www.tiktok.com/@apifytech",
    "x": "https://x.com/apify"
  },
  "contactPageUrl": "https://apify.com/contact",
  "contactFormUrl": "https://apify.com/contact-sales",
  "otherEmailsSkipped": 0,
  "pagesChecked": 4
}
```

### How to use it

1. Paste websites into **Websites**, one per line. `acme.com`, `www.acme.com` and `https://acme.com/` all work.
2. Optional: set **Pages per website** (default 4: the homepage plus up to 3 contact, imprint or about pages it links to). You pay per company, not per page.
3. Click **Start**. Export as CSV, Excel or JSON, or call it from the API, Make, Zapier or n8n.

### Enrich Google Maps leads or search results

Already scraped a list of businesses? Pick that run's dataset in **Dataset with websites** and the Actor reads the websites from it.

- **Google Maps leads:** run a Google Maps scraper from Apify Store, then pick its dataset here. The `website` field is found automatically. Businesses without a website are skipped and cost nothing.
- **Google search results:** pick a Google Search Results Scraper dataset and set **Website field in the dataset** to `organicResults.url`.
- **Keep your lead's fields:** list them in **Copy fields from the dataset** (for Google Maps: `title`, `placeId`, `address`, `categoryName`, `totalScore`) and each row gets them under `source`, ready to merge in a spreadsheet.
- **Run it automatically after the other Actor:** on the other Actor's **Integrations** tab, add this Actor and set its input to `{"datasetId": "{{resource.defaultDatasetId}}"}`.

The Actor only reads the one dataset you pick; it can't see your other data. Duplicate websites are visited once.

### Scheduled lead lists: pay only for new companies

Enriching the same kind of list every week, such as a scheduled Google Maps scrape? Turn on **Skip companies found in earlier runs**. Websites whose contacts an earlier run already found are skipped and cost nothing, so each run only charges for companies you haven't got yet. Websites where nothing was found are tried again next time, since sites change.

- The list of found companies is kept in your own Apify account, in a named key-value store (`business-contact-finder-known`). Nobody else can read it.
- Use **List name** to keep separate lists, for example one per client or campaign.
- `www.acme.com`, `acme.com` and `https://acme.com/contact` count as the same company; different subdomains (`shop1.myshopify.com`, `shop2.myshopify.com`) count as different ones.
- The `SUMMARY` record shows how many websites were skipped (`skippedKnown`).

### How it crawls

- **Polite:** it reads robots.txt first and obeys it for every page (RFC 9309; if robots.txt can't be read because of a server error, the site is treated as off limits). Pages of one website are fetched one after another, with a pause between them. It identifies itself honestly as `BusinessContactBot`.
- **No tricks:** sites that answer with an anti-bot challenge are reported as `blocked_by_bot_protection`. The Actor never tries to get past them, never logs in, and leaves addresses that a site hides from bots (Cloudflare email obfuscation) hidden.
- **Safe:** it refuses IP addresses and internal host names and connects only to public addresses.

### Pricing

You pay per company where at least one contact point was found (an email, phone, social page or contact page), plus Apify's standard start fee of $0.00005 per run. Websites skipped because an earlier run found them, websites where nothing was found, that don't exist, can't be reached, answer with an HTTP error, block bots or disallow crawling in robots.txt, parked or for-sale domains, and invalid inputs have no per-result charge.

**Only want emails?** Turn on **Only companies with an email**. Websites where no company email was found then have no per-result charge either: they are listed with status `no_email`, and their phones, social pages and contact page are left out. You pay only for companies where a company email was found. Set a maximum cost per run in the run options, and the Actor stops when it reaches it.

### Limitations

- **Sites built with JavaScript only** (some Wix, Squarespace and app-style sites) may show no contacts, because the Actor reads the HTML the server sends and doesn't run a browser. Those rows have no per-result charge.
- **Role addresses only,** by design. If a company lists only a person's address, the row shows `otherEmailsSkipped` instead.
- **Phone numbers** are taken as the site publishes them; they aren't verified.
- **Only the pages the homepage links to** with contact, imprint or about words in the link (in English, German, French, Spanish, Italian and Portuguese) are read.

### Your responsibilities

The output can still relate to people: a small business's info@ address or phone number may reach its owner. You decide what you do with the data, so make sure you have a lawful basis under the privacy and marketing laws that apply to you (for example GDPR, PECR or ePrivacy rules, CAN-SPAM, CASL) before contacting anyone, and honour opt-outs. Apify's guide to [the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) is a good starting point.

### Related Actors

- [Tech Stack & Domain Lookup](https://apify.com/stellalytics/website-domain-intelligence): the same companies' tech stack, email provider (Google Workspace or Microsoft 365), domain age and SSL expiry. It reads Google Maps datasets too.
- [Bulk URL Status Checker](https://apify.com/stellalytics/bulk-url-status-checker): check which lead websites are live, broken or redirecting.

### FAQ

**Why one row per company?** Lead lists are per company. One row with the best email, all phones and social pages is ready to import into a CRM without de-duplicating pages.

**Does it find people's names or personal emails?** No. That is a deliberate choice: it looks for the ways a company asks to be contacted.

**Can it run on a schedule?** Yes. Save your input as a task and add it to a schedule in Apify Console.

### Support

Found a bug or need a feature? Open an issue on the Issues tab, where fixes for broken runs are a priority. For a custom version or a question you'd rather not post publicly, email <stellalytics@gmail.com>. Never send API keys or passwords by email or in an issue.

This Actor is not affiliated with Google, LinkedIn or any site it reads.

# Actor input Schema

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

Company websites, one per line: acme.com or https://www.acme.com/. Duplicates are removed. IP addresses and internal hosts are refused.

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

The homepage plus contact, imprint and about pages found on it. More pages find more contacts but take longer. You pay per company, not per page.

## `includePhones` (type: `boolean`):

Company phone numbers from tel: links and the site's structured data (schema.org). Numbers marked as a person's are never read.

## `checkEmailDomain` (type: `boolean`):

Look up the email domain's mail servers (MX records) in DNS and set emailDomainAcceptsMail to true or false (null when DNS didn't answer). Only DNS is asked; no mail server is contacted and no email is sent, so it does not prove a single mailbox exists. Free.

## `onlyWithEmail` (type: `boolean`):

Turn on if you only want companies that have a company email (for example for email outreach). Websites where no company email was found then cost nothing: they are listed with status no_email, and their phones, social pages and contact page are left out. Off: any contact point counts and is charged.

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

How many websites to visit at the same time. Pages of one website are always fetched one after another.

## `skipKnownCompanies` (type: `boolean`):

For lists you enrich again and again (for example a weekly Google Maps scrape): websites whose contacts an earlier run already found are skipped and cost nothing, so you only pay for new companies. Websites where nothing was found are tried again. The list is kept in a named key-value store in your own Apify account (business-contact-finder-known).

## `historyName` (type: `string`):

Optional. Keep separate lists of found companies, for example one per client or campaign. Runs with the same name share one list. Leave empty for the default list.

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

Optional. Find contacts for the websites in a dataset from another run, for example Google Maps scraper leads (field 'website') or Google Search Results Scraper results (field 'organicResults.url'). Rows without a website are skipped and cost nothing. The sample websites above are ignored when a dataset is given.

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

Leave empty to detect it (website, domain, url, companyWebsite, homepage, link, organicResults.url and similar). Use dots for nested lists, for example organicResults.url.

## `keepSourceFields` (type: `array`):

Optional. Top-level fields of the dataset's rows to copy into each result under 'source', so you can match contacts back to your leads. For a Google Maps scraper dataset, for example: title, placeId, address, categoryName, totalScore. Up to 10 fields; only text, numbers and true/false are copied.

## Actor input object example

```json
{
  "websites": [
    "apify.com",
    "python.org"
  ],
  "maxPagesPerSite": 4,
  "includePhones": true,
  "checkEmailDomain": true,
  "onlyWithEmail": false,
  "maxConcurrency": 20,
  "skipKnownCompanies": false
}
```

# Actor output Schema

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

No description

## `summary` (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",
        "python.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("stellalytics/business-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",
        "python.org",
    ] }

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

```

## MCP server setup

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