# Contact Details Scraper & Google Maps Email Extractor (`hiver/company-contact-details`) Actor

Contact details scraper and Google Maps email extractor. Enrich Google Maps Scraper results or any website list with emails, phones, socials and address: one row per place, each with its source page. Pay only for sites with contacts, $2/1k. Most accurate of 4 on a 6-site bench (10-09).

- **URL**: https://apify.com/hiver/company-contact-details.md
- **Developed by:** [hiver](https://apify.com/hiver) (community)
- **Categories:** Lead generation, Social media, Integrations
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.70 / 1,000 website 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 & Google Maps Email Extractor

**A contact details scraper and Google Maps email extractor. Point it at your Google Maps Scraper dataset or paste company websites, and get one row per place or company: role emails, phone numbers, social profiles and the postal address, each with the page it came from.** It reads each business's own website: role emails (info@, sales@, support@, press@), phones, LinkedIn / X / Facebook / Instagram / YouTube / GitHub / TikTok profiles, the contact page and the postal address when one is published. You pay only for websites where it found at least one contact detail.

Built for sales and lead-gen lists, local-business outreach from Google Maps results, CRM enrichment and market research. Company-level data only.

![Company websites in, a contact list out: real output rows with emails, phones, social profiles and postal address, each with its source page](https://raw.githubusercontent.com/hiver-data/company-contact-details/main/media/contact-details/readme.gif)

[Watch the 1-minute walkthrough (MP4)](https://github.com/hiver-data/company-contact-details/blob/main/media/contact-details/walkthrough.mp4) · [Python and Node.js examples on GitHub](https://github.com/hiver-data/company-contact-details)

*Unofficial. Not affiliated with, endorsed by or sponsored by any website or company it reads.*

### Enrich Google Maps Scraper results with emails

Already ran a Google Maps scraper? Give this Actor the run's dataset ID. It reads the `website` of every place and returns **one row per place, in the dataset's order, and nothing else**. The place's own fields stay under `place` (`title`, `placeId`, `url`, `address`, `phone`, `categoryName`, whichever exist), next to the emails, phones, socials and address found on its website.

**Automatic after every Maps run (Apify integration):**

1. In Apify Console, open your Google Maps scraper and select the **Integrations** tab (**Add integration** if it already has some).
2. In the **Add integration** catalog, use the search field to find this Actor (Contact Details Scraper & Google Maps Email Extractor, by hiver) and click its card.
3. Set **Start when** to "run succeeded". This Actor's integration input is preset to the line below, so Apify fills in each Maps run's dataset ID. If your form shows something else, paste it:

```json
{
  "datasetId": "{{resource.defaultDatasetId}}"
}
```

4. Save, then use Apify's integration test menu on one of your past Maps runs to check the output before the next real run (a test is a real run and is billed like one).

Add `"maxItems": 100` (any number) to cap how many places are read from each Maps run, places without a website included. Each run is then charged for at most that many websites.

**Manual run, with a dataset you already have:** paste its ID into **Source dataset ID**:

```json
{
  "datasetId": "YOUR_GOOGLE_MAPS_DATASET_ID"
}
```

**The form's example domains are not added to your dataset run.** The input form comes prefilled with two example domains (`stripe.com`, `allbirds.com`). When you give a dataset and leave that list exactly as it is, it is ignored: you get only your places and pay only for them. If you edit the list, every domain in it is processed after the dataset rows.

`websiteField` defaults to `website` and takes nested paths such as `contact.website`, so any dataset with a website column works. The dataset input also works in limited-permission runs. A dataset the run cannot read fails with an explicit error; it is never treated as empty.

**What a Maps dataset costs:** $2 per 1,000 websites where at least one contact detail was found. These rows are free: places with no website (`no_website`), places whose website is only a Facebook or Instagram page (`website_is_social_profile`), unreachable sites (`failed`) and sites that publish nothing (`no_contacts_found`). Places that share a website, such as chain locations, keep their own rows, but the website is fetched and charged once per run. The first usable URL for a domain is the one read, so these are company-level contacts, not branch-specific ones. If a run reaches your maximum charge it stops, and its status message says so.

**Board test run, build 0.2.13 (2026-10-11), with the form's example domains left in:** input `{"datasetId": "eiFr4LxqWFyfrFoOg", "domains": ["stripe.com", "allbirds.com"], "maxItems": 5}`. That public 5-place sample dataset has 2 places sharing plausible.io, 1 place on posthog.com, 1 place with no website and 1 place whose website is a Facebook page. The run returned exactly 5 rows in the dataset's order, no Stripe or Allbirds row and 0 error rows in 7.7 s, and Apify recorded **2 charges**: plausible.io once and posthog.com once. The no-website and Facebook rows cost nothing. Try the same sample from the [Google Maps example task](https://apify.com/hiver/company-contact-details/examples/emails-for-google-maps-scraper-results). One row from that run, shortened (the place block comes from the sample dataset, not a live Maps listing):

```json
{
  "place": {
    "title": "Plausible Analytics",
    "placeId": "example-place-1",
    "address": "Västriku tn 2, 50403 Tartu, Estonia",
    "categoryName": "Software company"
  },
  "website": "https://plausible.io",
  "companyName": "Plausible Analytics",
  "status": "ok",
  "emails": [
    {
      "email": "hello@plausible.io",
      "type": "general",
      "sourceUrl": "https://plausible.io/contact"
    }
  ],
  "socials": {
    "x": "https://x.com/PlausibleHQ",
    "linkedin": "https://www.linkedin.com/company/plausible-analytics",
    "github": "https://github.com/plausible"
  },
  "address": "Västriku tn 2, 50403 Tartu",
  "addressSourceUrl": "https://plausible.io/imprint"
}
```

The place without a website came back as `{"place": {"title": "Example place without a website", ...}, "status": "no_website", "emails": []}` and was not charged.

**Compared with Apify's dataset enricher** (Apify Store, checked 2026-10-11): compass/enrich-google-maps-dataset-with-contacts bills Apify platform usage, so its cost depends on each run; 566 of its 634 public runs in the last 30 days succeeded, and it is rated 3.1 from 9 reviews. Here the price per website is fixed and shown before you start, and only websites with contacts found are charged. Accuracy on Maps datasets has not been benched against other Actors yet; the accuracy table below measures website lists.

### Most accurate of four contact scrapers in our bench

Our board's Lab bench ran this Actor and three other contact-details Actors from the Apify Store on the same 6 company websites (3 SaaS companies, 2 small businesses, 1 language school whose site redirects by locale) and checked 17 facts the companies publish: emails, phones, social profiles and postal addresses. Bench run 2026-10-09 20:59 UTC, build 0.2.6 (historical measured build):

| | This Actor | jfaro19/website-contact-finder | poidata/contact-details-scraper | solidscrape/contact-details-scraper |
|---|---|---|---|---|
| Known facts returned correctly | **100%** (17 of 17) | 76% (13 of 17) | 65% (11 of 17) | 59% (10 of 17) |
| Fields filled | **59%** | 56% | 55% | 44% |
| Bench runs that succeeded | 3 of 3 | 3 of 3 | 3 of 3 | 3 of 3 |
| Average run time per case | 31.9 s | 4.8 s | 44.5 s | 16.4 s |
| What a Store user paid per 1,000 results in these runs | $1.91 | $3.03 | $2.03 | $1.28 |

- **Postal address:** in the earlier bench run of the same 6 sites (2026-10-09 12:33 UTC, build 0.2.3), only this Actor returned the postal address plausible.io publishes on its imprint page (`Västriku tn 2, 50403 Tartu`, with `addressSourceUrl` `https://plausible.io/imprint`). solidscrape/contact-details-scraper has no address field.
- **Read it fairly:** six websites are a small sample. Fields filled is 3 points ahead: the most complete in this bench, not a big gap. jfaro19/website-contact-finder is much faster, and solidscrape/contact-details-scraper costs less per result. The bench covers these three Actors only, not every contact scraper on the Store, and it runs again after every new build.

### Why use it

- **One row per source place**, not one row per page. Chain locations sharing a website stay separate; their website is fetched and charged only once per run.
- **Postal address with its source:** `address` as one line plus `addressSourceUrl`, read from the site's schema.org PostalAddress data and its imprint/legal and contact pages. `null` when none is found (anchorbrewing.com in the bench).
- **Reads only a few pages**: the home page plus the contact, about, imprint/legal and support pages it links to (default 5 pages, 12 max).
- **Company channels, not private people.** Role mailboxes (info@, sales@, support@, press@, careers@...) are kept. A branded business mailbox published in the site's own Organization/ContactPoint structured data is also accepted, including Gmail-style addresses, with type `published_company`. Personal/unclassified addresses, detached contact points and explicitly foreign organizations are excluded.
- **Every item has its source page** (`sourceUrl`), so you can check it in one click.
- **Clear reasons when a site fails**: `connection_refused_or_blocked_by_site`, `robots_txt_disallows_home_page`, `domain_for_sale_or_parked`, `dns_not_found`, `ssl_error`, `blocked_by_site_http_403`, `login_required`, `rate_limited_429`... Failed sites are not charged.

### Input

- `domains`: company domains or URLs (`stripe.com`, `https://www.example.com/en`), deduplicated by registered domain. Supply `domains`, `datasetId`, or both. With a `datasetId`, the form's unchanged example list (`stripe.com`, `allbirds.com`) is ignored.
- `datasetId`: an existing Google Maps Scraper (or other) dataset ID; see Enrich Google Maps Scraper results with emails above. `websiteField` defaults to `website`; nested paths such as `contact.website` work. Optional `maxItems` caps the source dataset items read, including items without websites, not the separate domains list. Dataset rows come first in their original order.
- `maxPagesPerSite` (default 5), `maxConcurrency` (default 5), `requestDelayMs` (default 600 ms between pages of the same site).
- `defaultCountry`: reads local phone numbers written without a country code. Without an override, `.cn` sites use CN; explicit `0086` phone numbers are normalized to `+86`, and international CN tel links supply the region for national footer text. Phones are deduplicated by E.164. Same-site locale redirects such as `/jp` and `/en` are followed, and the resulting page URL is retained as the source.
- `allowOtherEmailDomains`, `allowFreemailAddresses`: off by default.
- `retryBlockedWithProxy` (default true): retry HTTP 403 once per site through included Apify datacenter Proxy. The User-Agent stays `hiver-company-contact-details/0.2 (+https://apify.com/hiver/company-contact-details)` on both routes; it never impersonates a browser. Successful retries keep the same route for robots.txt and subsequent pages; there is no IP rotation. Set false for direct-only operation.
- `proxyConfiguration`: optional explicit proxy for all requests. Residential groups are not supported in this build and are rejected before requests; no residential usage is silently added to your bill.

### Output (real run from the 2026-10-09 bench, shortened)

```json
{
  "input": "https://plausible.io",
  "domain": "plausible.io",
  "companyName": "Plausible Analytics",
  "status": "ok",
  "emails": [
    {
      "email": "hello@plausible.io",
      "type": "general",
      "sourceUrl": "https://plausible.io/contact"
    }
  ],
  "phones": [],
  "socials": {
    "x": "https://x.com/PlausibleHQ",
    "linkedin": "https://www.linkedin.com/company/plausible-analytics",
    "github": "https://github.com/plausible"
  },
  "contactPageUrl": "https://plausible.io/contact",
  "address": "Västriku tn 2, 50403 Tartu",
  "addressSourceUrl": "https://plausible.io/imprint",
  "pagesChecked": [
    "https://plausible.io",
    "https://plausible.io/contact",
    "https://plausible.io/imprint",
    "https://plausible.io/about",
    "https://plausible.io/wordpress-analytics-plugin"
  ]
}
```

Fields per company: `domain`, `website`, `companyName`, `status` (`ok`, `no_contacts_found`, `failed`, `no_website`, `website_is_social_profile`), `failureReason`, `emails[]`, `phones[]`, `socials{}`, `socialSources{}`, `otherSocialCandidates`, `contactPageUrl`, `address`, `addressSourceUrl`, `pagesChecked[]`, `redirectedToDomain`, `scrapedAt`, `proxyRetry` (network, status, and exception type only when unavailable/failed; never proxy credentials).

Each email/phone includes `foundVia` (`structured_data`, `link`, `address_tag`, `decoded_obfuscation`, `page_text`) and `pageType` (home/contact/imprint/about/support/press/legal, or `other`); the address uses `addressFoundVia` / `addressPageType` (both `null` without an address). These describe the first retained source, not verification or confidence; check `sourceUrl` before use.

### Price

Pay per event: **$2 per 1,000 websites where at least one contact detail was found** on the Free plan (`company-processed`, $0.002), less on paid plans. No website, a social-profile-only website, an unreachable site, an invalid input or nothing found = no charge. Each distinct website is charged once per run, even when several places share it. List prices of the benched rivals, checked 2026-10-09: jfaro19/website-contact-finder $3.00, poidata/contact-details-scraper $2.00 and solidscrape/contact-details-scraper $1.25 per 1,000 results. For comparison, a tool that bills per crawled page at $2 per 1,000 pages costs about $0.01 for a 5-page site (prices checked 2026-10-07; check current pricing before you compare).

### Coverage

Not every company publishes an email address or a phone number. Unreachable or refusing sites produce an explicit failure row, not guessed contact details. A business may publish only social profiles or a contact form. Check `pagesChecked` and the source URL of each returned field.

### Good to know

- Reads the HTML a server sends. Sites that build their pages only in the browser with JavaScript return `no_contacts_found` with the reason `page_builds_content_with_javascript`.
- Many large companies publish only contact forms, not emails: you will get their social links and phones, but `emails` can be empty. That is the truth of the site, not an error.
- Respects `robots.txt` (and Crawl-delay up to 10 s). The one Apify datacenter retry is limited to HTTP 403, not logins (401), rate limits (429), explicit robots disallows or CAPTCHAs. Persistent refusal is reported as failed.
- A phone number found on a company's page is the company's published number. International formatting is applied with the `phonenumbers` library; check numbers before bulk calling and follow your local telemarketing and anti-spam laws.
- Use the data lawfully. You are responsible for how you contact the people behind these addresses (GDPR, CAN-SPAM, PECR and similar).

### Pricing table & example

Pay per event. Prices per 1,000 events, by Apify plan (higher plans get a discount automatically):

| Event | Free | Starter (Bronze) | Scale (Silver) | Business (Gold) |
|---|---|---|---|---|
| Website with contacts found (`company-processed`) | $2.00 | $1.90 | $1.80 | $1.70 |

**Worked example:** 5,000 billable `company-processed` events on the Free plan cost 5,000 × $0.002 = **$10.00**; on Gold, 5,000 × $0.0017 = $8.50. Rows that the Price section lists as free cost nothing. Apify platform usage is included in these prices.

### Input example

```json
{
  "domains": [
    "stripe.com",
    "allbirds.com"
  ]
}
```

### FAQ

**Does it search Google Maps itself?**\
No. It reads a dataset your Google Maps scraper already produced (any dataset with a website field). To find places, run a Maps scraper first, then pass its dataset ID or add this Actor as its integration.

**Does it find personal emails?**\
No. It returns role addresses and branded business mailboxes explicitly published by the site's own organization in structured data. Unclassified personal addresses are skipped and counted; a Gmail-style mailbox is not automatically treated as personal or as company-owned.

**How many companies get results?**\
Not every site publishes contact details. See Coverage above; unreachable sites are not charged.

**What is not covered?**\
No login-only pages, no email guessing or verification, no data bought from third parties. It respects robots.txt.

**Is this official?**\
No. Unofficial and not affiliated with, endorsed by or sponsored by any company or site named in the results or this README.

**Something looks wrong or you need a field added?**\
Open an issue from the Actor's Issues tab with the input and run link.

# Actor input Schema

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

Company domains or website URLs, one per company (for example `stripe.com` or `https://www.example.com`). You get one result row per entry.

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

Home page plus the contact, about, imprint/legal and support pages linked from it. 5 is enough for most sites.

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

How many websites are processed at the same time (1-20). Each website is fetched sequentially with a pause between pages.

## `requestDelayMs` (type: `integer`):

Polite delay between two requests to the same website.

## `maxEmailsPerCompany` (type: `integer`):

Keep at most this many role emails per company.

## `maxPhonesPerCompany` (type: `integer`):

Keep at most this many phone numbers per company.

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

Used to read phone numbers written without a country code, for example `US` or `DE`. Empty = guess from the website's domain ending, else US.

## `allowOtherEmailDomains` (type: `boolean`):

By default only role addresses on the company's own domain are kept (plus general/sales/press/support addresses listed on its contact or imprint page). Turn on to keep every role address found.

## `allowFreemailAddresses` (type: `boolean`):

Keep role freemail addresses found in ordinary page text or mailto links. Off by default. Role or branded business mailboxes explicitly published by the site's own Organization/ContactPoint structured data are retained even when this is off; personal addresses are still excluded.

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

Direct requests first. On HTTP 403, make one retry through included Apify datacenter Proxy, keeping the same disclosed bot identity. A successful retry keeps this route for robots.txt and subpages. No IP rotation, no login/rate-limit retries, and no override of robots rules. Residential is not supported.

## `proxyConfiguration` (type: `object`):

Optional explicit proxy for all requests. Otherwise direct requests are used with one automatic Apify datacenter retry on HTTP 403 (retryBlockedWithProxy). Residential groups are rejected before network access because they are not priced in this build. No login, CAPTCHA or robots bypass.

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

An existing Google Maps or other dataset. One result per item in its original order. May be combined with domains.

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

Field containing the company website; nested dot paths such as contact.website are supported.

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

Optional cap on dataset items read, including places without websites. Does not cap the separate domains list.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "allbirds.com"
  ],
  "maxPagesPerSite": 5,
  "maxConcurrency": 5,
  "requestDelayMs": 600,
  "maxEmailsPerCompany": 10,
  "maxPhonesPerCompany": 5,
  "allowOtherEmailDomains": false,
  "allowFreemailAddresses": false,
  "retryBlockedWithProxy": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "websiteField": "website"
}
```

# Actor output Schema

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

One row per source dataset place in input order, or per distinct domain in domains. place preserves source title, placeId, url, address, phone and categoryName. Shared websites reuse contacts; only distinct domains with status ok are charged. Missing and social-only websites are free. Emails, phones and address include extraction provenance, not verification.

# 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": [
        "stripe.com",
        "allbirds.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("hiver/company-contact-details").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": [
        "stripe.com",
        "allbirds.com",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("hiver/company-contact-details").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": [
    "stripe.com",
    "allbirds.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call hiver/company-contact-details --silent --output-dataset

```

## MCP server setup

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

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/E3xE5hjnUtngrVnUJ/builds/wBlwXcx6IG2QNcGL1/openapi.json
