# Contact Details Scraper: Website Emails, Phones and Socials (`joesimo/contact-details-finder`) Actor

Turn a list of company websites into leads: business emails, formatted phone numbers, social profiles and contact page URLs. Scans the homepage plus contact, imprint and about pages, ranks the shared inbox first. Pay only for websites where contacts were found, the rest are free.

- **URL**: https://apify.com/joesimo/contact-details-finder.md
- **Developed by:** [Joe Simo](https://apify.com/joesimo) (community)
- **Categories:** Lead generation, Social media, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 2 monthly users, 96.4% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 websites

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

## Contact Details Scraper: website emails, phone numbers and social profiles

**Paste a list of company websites, get back their public business contact details** as a clean table: email addresses, formatted phone numbers, LinkedIn, Facebook, Instagram, X/Twitter, YouTube, TikTok and 8 more social profiles, the contact page and contact form URL, and the company name and address when the site publishes them.

- **Pay only for websites where contacts were found:** $3 per 1,000 ($0.003 each). Dead domains, blocked sites and sites with no contacts are saved with a status and cost nothing, and extra pages per site are free.
- **Smarter than a homepage regex:** it reads the homepage plus up to 3 contact, imprint, about, locations or support pages, ranks emails so the shared business inbox comes first, and validates every phone number.
- **Fast and light:** plain HTTP, no browser, no proxy. 100 websites in about 35 seconds.

### Quick start

1. Click **Try for free**, keep the prefilled websites (or paste your own) and press **Start**.
2. Or copy this input into the **JSON** tab. It is also what runs when you call the API with an empty input:

```json
{
    "websites": ["allbirds.com", "katzsdelicatessen.com", "flourandwater.com"]
}
```

This run took 3.3 seconds in the Apify cloud on Nov 1, 2026 and found contacts on all 3 sites, so it costs 3 x $0.003 = $0.009. One of the 3 rows, unchanged except for the 14 per-platform social lists and timing fields being cut:

```json
{
    "input": "flourandwater.com",
    "domain": "flourandwater.com",
    "url": "https://www.flourandwater.com/",
    "status": "ok",
    "title": "Flour + Water",
    "companyName": "Flour + Water",
    "primaryEmail": "info@flourandwater.com",
    "primaryPhone": "+1 415 826 7000",
    "emails": ["info@flourandwater.com"],
    "phones": ["+1 415 826 7000"],
    "facebook": "https://www.facebook.com/flourandwater",
    "instagram": "https://www.instagram.com/flourandwatersf",
    "linkedin": null,
    "emailDetails": [
        { "email": "info@flourandwater.com", "isRole": true, "isSameDomain": true, "isFreeProvider": false,
          "foundOn": ["https://www.flourandwater.com/", "https://www.flourandwater.com/contact/"], "foundVia": "mailto" }
    ],
    "phoneDetails": [
        { "e164": "+14158267000", "international": "+1 415 826 7000", "national": "(415) 826-7000",
          "country": "US", "type": "FIXED_LINE_OR_MOBILE", "foundVia": "tel_link" }
    ],
    "contactPageUrl": "https://www.flourandwater.com/contact/",
    "counts": { "emails": 1, "phones": 1, "socialProfiles": 2 },
    "contactsFound": true,
    "charged": true
}
```

**Price: $0.003 per website with contacts found ($3 per 1,000), plus a $0.00005 run start fee.** Websites with nothing found, blocked or dead sites, and extra pages are free.

### What you get

Real results from the prefilled 6-website input on Nov 1, 2026 (shortened):

| Website | Primary email | Primary phone | Social profiles | Contact page |
|---|---|---|---|---|
| allbirds.com | help@allbirds.com | +1 424 363 8064 | 6 (Instagram, Facebook...) | /pages/contact-us |
| katzsdelicatessen.com | press@katzsdelicatessen.com | +1 212 254 2246 | 3 (Instagram, Facebook...) | /contact |
| flourandwater.com | info@flourandwater.com | +1 415 826 7000 | 2 | /contact/ |
| mailchimp.com | (none published) | +1 800 315 5939 | 6 (LinkedIn, Instagram...) | /contact/ |
| stripe.com | (varies, see below) | +1 888 926 2289 | 8 (LinkedIn, Instagram...) | /contact/sales |
| dishoom.com | all-ears@dishoom.com | (UK numbers, set `defaultCountry` to GB) | 5 (LinkedIn, Instagram...) | /group-feasts/enquiry/ |

All 6 have contacts, so this run costs 6 x $0.003 = $0.018. Each row also has every email and phone found (with the page and method it came from), one column per social network, `companyName`, `address`, and `status`. stripe.com A/B tests its contact page, so its sales address shows up in some runs and not others.

### Fields for every website

- **Emails** from mailto links, schema.org data and the page text, ranked so the best general business address comes first (`primaryEmail`). Each email is flagged as role (info@, sales@), same domain as the site, or free provider (Gmail, Outlook).
- **Phone numbers** validated and formatted (E.164, international and national format, country, line type) with `libphonenumber`. Local numbers are read using the site's country (domain ending or page language) or your `defaultCountry`.
- **Social profiles** for 14 platforms, with one flat column per major network for spreadsheets. Share buttons, intent links and the platforms' own pages are filtered out.
- **Contact page and contact form URL**, so you can follow up by hand when a site has no email.
- **Company name and postal address** when the site publishes them as schema.org data.
- **Evidence:** every email and phone lists the pages it was found on and how (mailto, tel, structured data or text).

### Use cases

- **Lead generation:** turn a list of domains from Google Maps, a directory or a CRM export into contactable leads.
- **CRM enrichment:** fill in missing emails, phones and LinkedIn company pages for your accounts.
- **Agencies and freelancers:** find who to contact at local businesses you want to pitch.
- **Partnership, PR and supplier outreach:** get the general inbox and the contact form in one pass.
- **Data cleaning:** check which of your accounts still publish a working contact page.

### Example inputs

Copy one of these into the **JSON** tab of the input form. Each was tested on Nov 1, 2026.

**1. A list of company websites (domains or full URLs).**

```json
{
    "websites": ["stripe.com", "allbirds.com", "katzsdelicatessen.com", "flourandwater.com", "dishoom.com", "mailchimp.com"]
}
```

**2. Clean shared business inboxes only (info@, sales@, help@ on the company's own domain).** Personal-looking and off-domain addresses are dropped.

```json
{
    "websites": ["mailchimp.com", "allbirds.com", "katzsdelicatessen.com", "flourandwater.com"],
    "sameDomainEmailsOnly": true,
    "roleEmailsOnly": true
}
```

**3. Paste a column straight from a spreadsheet or CSV.** One website per line or separated by commas. A header line such as `website` is ignored.

```json
{
    "websitesText": "website\nallbirds.com\nhttps://www.katzsdelicatessen.com/\nflourandwater.com, mailchimp.com"
}
```

**4. Enrich the output of another Actor, for example a Google Maps scraper.** Use the dataset ID from that run and the name of its website field (nested fields such as `company.website` work too).

```json
{
    "datasetId": "YOUR_DATASET_ID",
    "datasetField": "website"
}
```

**5. UK businesses, more pages per site.** `defaultCountry` tells the Actor how to read local phone numbers when the domain does not reveal the country (dishoom.com returns 13 UK numbers with GB and none without it).

```json
{
    "websites": ["dishoom.com", "monmouthcoffee.co.uk", "pret.co.uk"],
    "defaultCountry": "GB",
    "maxPagesPerSite": 6
}
```

### Input

| Field | Description |
|---|---|
| `websites` | Domains or URLs to scan, for example `stripe.com` or `https://www.example.co.uk`. |
| `websitesText` | Bulk paste: one website per line, or separated by commas, spaces or semicolons. Header lines like `website` are ignored. |
| `datasetId` / `datasetField` | Read websites from an Apify dataset, for example the output of a Google Maps or lead list Actor. The field can be nested (`company.website`). |
| `maxPagesPerSite` | Pages per website, homepage included (default 4, max 10). |
| `includePhones` | Collect and format phone numbers (on by default). |
| `defaultCountry` | Country for local phone numbers when the site does not reveal it (default US). |
| `sameDomainEmailsOnly` | Keep only addresses on the site's own domain. |
| `roleEmailsOnly` | Keep only generic business addresses such as info@ and sales@. |
| `respectRobotsTxt` | Skip pages that robots.txt disallows (on by default). |
| `dedupe` | Scan each domain once (on by default). |
| `maxItems` | Stop after this many websites (0 = no limit). |
| `maxConcurrency` | Websites scanned in parallel. Automatic by default: 6 at 256 MB, 10 at 512 MB, 16 at 1024 MB. |
| `timeoutSecs` | Timeout per page (default 20 seconds). A website gets at most 4 times this. |
| `userAgent` | Optional custom User-Agent. |

### Output

One dataset row per website. Full field list, shown on a made-up UK site so the `address` and `contactFormUrl` fields are filled (see Quick start above for a real row):

```json
{
    "input": "examplebakery.co.uk",
    "domain": "examplebakery.co.uk",
    "url": "https://examplebakery.co.uk/",
    "status": "ok",
    "companyName": "Example Bakery",
    "primaryEmail": "hello@examplebakery.co.uk",
    "primaryPhone": "+44 117 496 0000",
    "emails": ["hello@examplebakery.co.uk", "orders@examplebakery.co.uk"],
    "phones": ["+44 117 496 0000"],
    "facebook": "https://www.facebook.com/ExampleBakery",
    "instagram": "https://www.instagram.com/examplebakery",
    "linkedin": null,
    "emailDetails": [
        { "email": "hello@examplebakery.co.uk", "isRole": true, "isSameDomain": true, "isFreeProvider": false,
          "foundOn": ["https://examplebakery.co.uk/contact/"], "foundVia": "mailto" }
    ],
    "phoneDetails": [
        { "e164": "+441174960000", "international": "+44 117 496 0000", "national": "0117 496 0000",
          "country": "GB", "type": "FIXED_LINE", "foundVia": "tel" }
    ],
    "contactPageUrl": "https://examplebakery.co.uk/contact/",
    "contactFormUrl": "https://examplebakery.co.uk/contact/",
    "address": { "street": "1 High Street", "city": "Bristol", "region": null, "postalCode": "BS1 1AA", "country": "GB" },
    "counts": { "emails": 2, "phones": 1, "socialProfiles": 2 },
    "contactsFound": true,
    "pagesScanned": [
        { "url": "https://examplebakery.co.uk/", "kind": "home", "httpStatus": 200 },
        { "url": "https://examplebakery.co.uk/contact/", "kind": "contact", "httpStatus": 200 }
    ],
    "charged": true,
    "error": null
}
```

`status` is one of `ok`, `http_error`, `unreachable`, `robots_disallowed` or `invalid_input`. Only `ok` rows with `contactsFound: true` are charged, and `charged` tells you which ones were. The run's `OUTPUT` record in the key-value store has totals (websites, ok, with contacts, charged, emails, phones, social profiles).

### Pricing

**$3 per 1,000 websites with contacts found** ($0.003 each), plus the tiny Apify run start fee. Nothing else: websites with no contacts, failed websites and invalid inputs are free, and so is the extra page loading. Set a **Maximum cost per run** and the Actor stops cleanly when it is reached.

| Run | Websites | Typical cost |
|---|---|---|
| Prefilled input in the Console | 6 | $0.018 |
| Empty input via the API (3 demo sites) | 3 | $0.009 |
| 100 brand websites (our test: 79 had contacts) | 100 | about $0.24 |
| 1,000 local businesses from Google Maps | 1,000 | at most $3, less for sites with no contacts |

In our test on 100 well known brand websites, 79 had contacts (email 59%, phone 42%, social 88%). Small business sites usually publish more contact details than big brands. With Apify's free monthly credit you can scan well over a thousand websites before paying anything.

### Integrations and API

- **API:** run it and get the rows back in one call, for example `POST https://api.apify.com/v2/acts/joesimo~contact-details-finder/run-sync-get-dataset-items?token=YOUR_TOKEN` with the input as the JSON body. See the **API** tab for JavaScript, Python and cURL code.
- **No-code:** connect it with **Make**, **Zapier** or **n8n**, or push results to **Google Sheets** or a webhook when a run finishes.
- **Chain Actors:** point `datasetId` at the output of a Google Maps, directory or lead list Actor, then pass the emails to an email validator.
- **AI agents:** callable as a tool through the Apify MCP server.
- **Export:** CSV, Excel, JSON, XML or HTML. The flat columns (`primaryEmail`, `primaryPhone`, `linkedin`, `facebook`...) drop straight into a spreadsheet or CRM.

### FAQ

**Do I pay for websites where nothing was found?**
No. Only rows with `status: ok` and at least one email, phone or social profile are charged, and the `charged` field shows which ones were.

**Does it find personal emails of employees?**
No. It only reads what the website publishes on its own contact-type pages. It does not guess email patterns, visit team or careers pages, or look people up.

**Why is a site marked `http_error`?**
Usually bot protection (HTTP 403) or a server error. The Actor does not bypass protection, and these rows are free.

**Can it read emails hidden behind JavaScript or "name \[at] domain"?**
No, see the limits below. The contact page and contact form URL are always returned so you can follow up by hand.

**How fast is it?**
About 100 websites in 35 seconds at the default 512 MB memory.

### Honest limits

- **No JavaScript rendering.** Details that only appear after scripts run (some Wix and single page app sites, chat widgets, map embeds) are not seen.
- **Hidden emails are not decoded.** Addresses protected by Cloudflare email obfuscation or written as "name \[at] domain" are skipped. `protectedEmailsSkipped` counts the Cloudflare ones so you know to check the contact page by hand.
- **No bot protection bypass.** Sites behind bot protection answer 403 and are saved as `http_error` for free. About 13% of large brand sites did this in our tests; small business sites rarely do.
- **robots.txt is respected** by default, and the Actor identifies itself with an honest User-Agent.
- **Public business contacts only.** It reads what the website itself publishes. It does not guess email patterns, look up people, or collect names, job titles or personal profiles from team pages (team, careers and login pages are never visited, and schema.org Person data is ignored).
- **Same website only.** Pages on other domains and subdomains (blogs, help centers, shops on other hosts) are not followed.

### Responsible use

The data comes from public web pages, but emails and phone numbers can still be personal data under GDPR, CCPA and similar laws, for example a sole trader's own address. You are responsible for having a lawful basis to store and use it, and for following anti spam rules (CAN-SPAM, PECR) when you contact anyone. Do not use the output for unsolicited bulk email.

### Tips

- Feed it the output of a Google Maps or directory Actor with `datasetId` and `datasetField` set to the website field.
- Turn on `sameDomainEmailsOnly` and `roleEmailsOnly` for clean, shared business inboxes only.
- Raise `maxPagesPerSite` to 6 or 8 for large sites with many location pages. It costs a little more compute but not more per website.
- Pair it with an email validator to check the addresses before outreach.

### Support

Found a site where a published contact was missed, or a wrong result? Open an issue on the Actor's Issues tab with the domain and we will look at it. Contact: joesimo@mail.grokbot.com

More examples and the docs for all 6 of our Actors: [github.com/Joe-Simo/apify-actors](https://github.com/Joe-Simo/apify-actors/tree/main/contact-details-finder). Store page: https://apify.com/joesimo/contact-details-finder

# Actor input Schema

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

Company domains or URLs to scan, for example stripe.com or https://www.example.co.uk. The Actor loads the homepage (with www and HTTP fallbacks), then up to a few contact, imprint, about and location pages linked from it. If you start a run with no websites, no bulk paste and no dataset (for example an API call with <code>{}</code>), it scans allbirds.com, katzsdelicatessen.com and flourandwater.com as a demo (about $0.009).

## `websitesText` (type: `string`):

Paste many domains or URLs at once, one per line or separated by commas, spaces or semicolons. Combined with the list above.

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

Read websites from an existing Apify dataset, for example the output of Google Maps or another lead list Actor. Use together with the field name below.

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

Field in each dataset item that holds the website or domain. Dot paths such as company.website are supported. Default: website.

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

Homepage plus the best contact-type pages linked from it (contact, imprint, about, locations, support). 4 finds most published contacts. More pages take longer but the price per website stays the same.

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

Collect phone numbers from tel: links, schema.org data and labelled numbers on the page (Phone:, Tel, Call us). Numbers are validated and formatted in international format.

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

Two-letter country code used to read local numbers written without a country code, when the site's domain ending or page language does not reveal the country. Example: US, GB, DE.

## `sameDomainEmailsOnly` (type: `boolean`):

Keep only addresses like info@company.com for company.com. Drops Gmail, Outlook and agency addresses.

## `roleEmailsOnly` (type: `boolean`):

Keep only generic business addresses such as info@, sales@, hello@, support@. Drops addresses that look personal.

## `respectRobotsTxt` (type: `boolean`):

Skip pages that the site's robots.txt disallows for this bot. A site whose homepage is disallowed is saved with status robots_disallowed and is not charged.

## `dedupe` (type: `boolean`):

Scan each website only once, even if it appears several times in the input.

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

Stop after this many websites. 0 means no limit.

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

How many websites are scanned at the same time (each loads its pages 2 at a time). Leave empty for automatic: 10 at 512 MB memory (the default), 16 at 1024 MB or more, 6 at 256 MB.

## `timeoutSecs` (type: `integer`):

Maximum time for loading one page, including redirects. A whole website gets at most 4 times this.

## `userAgent` (type: `string`):

Optional. By default the Actor identifies itself honestly as ContactDetailsFinder with a link to this page.

## Actor input object example

```json
{
  "websites": [
    "stripe.com",
    "allbirds.com",
    "katzsdelicatessen.com",
    "flourandwater.com",
    "dishoom.com",
    "mailchimp.com"
  ],
  "datasetField": "website",
  "maxPagesPerSite": 4,
  "includePhones": true,
  "defaultCountry": "US",
  "sameDomainEmailsOnly": false,
  "roleEmailsOnly": false,
  "respectRobotsTxt": true,
  "dedupe": true,
  "maxItems": 0,
  "timeoutSecs": 20
}
```

# Actor output Schema

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

One item per website: emails, phone numbers, social profiles, contact page and form, company name and address, with the page each detail was found on.

## `runSummary` (type: `string`):

Counts by status, websites with contacts, total emails, phones and social profiles found, duplicates removed.

# 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": [
        "stripe.com",
        "allbirds.com",
        "katzsdelicatessen.com",
        "flourandwater.com",
        "dishoom.com",
        "mailchimp.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("joesimo/contact-details-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": [
        "stripe.com",
        "allbirds.com",
        "katzsdelicatessen.com",
        "flourandwater.com",
        "dishoom.com",
        "mailchimp.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("joesimo/contact-details-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": [
    "stripe.com",
    "allbirds.com",
    "katzsdelicatessen.com",
    "flourandwater.com",
    "dishoom.com",
    "mailchimp.com"
  ]
}' |
apify call joesimo/contact-details-finder --silent --output-dataset

```

## MCP server setup

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