# Contact Details Scraper for Websites | $0.60/1K (`kazkn/website-contact-details-scraper`) Actor

Get one row per website with cleaned email addresses, E.164 phone numbers, social profile links, company names, and source pages. Process a list of domains or URLs and pay only for websites where contact details are found.

- **URL**: https://apify.com/kazkn/website-contact-details-scraper.md
- **Developed by:** [KazKN](https://apify.com/kazkn) (community)
- **Categories:** Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.60 / 1,000 results

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 Details Scraper

This **contact details scraper** turns a list of website URLs or domains into one structured dataset row per website, with published email addresses, phone numbers, social profiles, company names, and source pages. Use it to prepare lead lists, enrich company research, find a business contact page, or collect public contact channels without checking every website manually.

The Actor reads the site's HTML directly, then follows useful same-site links such as contact, imprint, legal notice, about, and privacy pages. It supports LinkedIn, Facebook, Instagram, X/Twitter, YouTube, and TikTok links. Every email and phone number includes the URL where it was first found, so you can inspect the source before using the data.

It is designed for sales and marketing teams, researchers, agencies, recruiters, and developers who need contact information from a known list of websites. It is a website scraper, not an email guessing tool: it returns details published on the pages it reads and does not invent addresses for people or companies.

### Real output example

The following rows were produced by a local run of the current code on November 8, 2026, using the default maximum of five pages per website. They are copied unchanged from that run's dataset, not mock data. Empty arrays and `null` values are included to show what the dataset returns when a particular detail is not found.

```json
[
  {
    "domain": "apify.com",
    "inputUrl": "https://apify.com",
    "status": "ok",
    "statusReason": "5 page(s) read",
    "billed": true,
    "pagesCrawled": 5,
    "emails": [
      "support@apify.com",
      "hello@apify.com",
      "legal@apify.com",
      "privacy@apify.com"
    ],
    "emailTypes": {
      "support@apify.com": "generic",
      "hello@apify.com": "generic",
      "legal@apify.com": "generic",
      "privacy@apify.com": "generic"
    },
    "emailSourcePages": {
      "support@apify.com": "https://apify.com/",
      "hello@apify.com": "https://apify.com/contact",
      "legal@apify.com": "https://docs.apify.com/legal",
      "privacy@apify.com": "https://docs.apify.com/legal"
    },
    "phones": [],
    "phoneSourcePages": {},
    "linkedin": "https://www.linkedin.com/company/apify",
    "facebook": null,
    "instagram": null,
    "twitter": "https://x.com/apify",
    "youtube": null,
    "tiktok": "https://www.tiktok.com/@apifytech",
    "contactPageUrl": "https://apify.com/contact",
    "legalPageUrl": "https://docs.apify.com/legal",
    "companyName": "Apify Technologies s.r.o.",
    "scrapedAt": "2026-11-08T05:32:08Z"
  },
  {
    "domain": "abcplumbing.com",
    "inputUrl": "https://abcplumbing.com",
    "status": "ok",
    "statusReason": "1 page(s) read",
    "billed": true,
    "pagesCrawled": 1,
    "emails": [
      "john@abcplumbing.com",
      "kevin@abcplumbing.com"
    ],
    "emailTypes": {
      "john@abcplumbing.com": "personal",
      "kevin@abcplumbing.com": "personal"
    },
    "emailSourcePages": {
      "john@abcplumbing.com": "https://abcplumbing.com/",
      "kevin@abcplumbing.com": "https://abcplumbing.com/"
    },
    "phones": [
      "+17195425631"
    ],
    "phoneSourcePages": {
      "+17195425631": "https://abcplumbing.com/"
    },
    "linkedin": null,
    "facebook": null,
    "instagram": null,
    "twitter": null,
    "youtube": null,
    "tiktok": null,
    "contactPageUrl": null,
    "legalPageUrl": null,
    "companyName": "ABC Plumbing Pueblo & Colorado Springs Colorado",
    "scrapedAt": "2026-11-08T05:32:07Z"
  }
]
```

A row with `status: "ok"` means that at least one email address, phone number, or supported social profile was found. It does not mean that every output field is populated. For example, a site with only social links can still be an `ok` result.

The main fields are:

- `domain` and `inputUrl`: the normalized website domain and the value supplied in the input.
- `status`, `statusReason`, and `billed`: the outcome, a plain-language explanation, and whether the row was billed as a result.
- `pagesCrawled`: the number of pages that loaded successfully for the website.
- `emails` and `phones`: deduplicated email addresses and phone numbers normalized to E.164 format.
- `emailTypes`: whether each address looks `generic` or `personal`, based on naming rules.
- `emailSourcePages` and `phoneSourcePages`: a map from each value to the first page where it was found.
- `linkedin`, `facebook`, `instagram`, `twitter`, `youtube`, and `tiktok`: the selected profile URL for each supported network, or `null`.
- `contactPageUrl`, `legalPageUrl`, and `companyName`: useful pages discovered during the crawl and the organization or site name when available.
- `scrapedAt`: the UTC extraction time in ISO 8601 format.

The run's key-value store also contains a `RUN_SUMMARY` record with website counts, billed results, statuses, error types, pages read, transfer volume, proxy charges, invalid inputs, and whether the run stopped because its cost limit was reached.

### Why choose this contact details scraper?

#### Contact-focused crawling, not just home-page extraction

A business often puts its email address on a contact page and its company details in an imprint or legal notice. The Actor starts with the supplied page, ranks same-site links, and prioritizes contact and legal pages before about and privacy pages. If the starting page has no contact or legal link, it can also try conventional paths such as `/contact`; German-speaking and French domains get relevant legal-page guesses.

The default limit is five pages per website and can be adjusted from 1 to 20. You pay per website result, not per page read, so a result assembled from several pages still counts once (the optional proxy fallback described below is the only per-page charge).

#### Cleaner emails with useful classification

Email extraction covers `mailto:` links, visible text, Cloudflare-obfuscated addresses, and common masked forms such as `name [at] company [dot] com`. Cleaning rules remove placeholders, documentation examples, Sentry and Wixpress artifacts, image-like strings, and common sample names.

Each surviving address is labeled `generic` or `personal`. Role addresses such as `info@`, `support@`, `sales@`, and `press@` are generic; addresses that look like a named person are personal. This classification is rule-based, not a statement about the actual owner. Two optional filters let you retain only generic addresses or only addresses on the website's own domain.

#### Normalized phone numbers and traceable sources

Phone numbers can come from `tel:` links, JSON-LD, labeled text, and valid-looking numbers in visible page text. The Actor uses phone-number parsing and validation rules, then returns numbers in E.164 format, such as `+17195425631`. This validates the number's format and numbering rules; it does not call the number or confirm that it is active. The source-page map lets you review context and identify numbers belonging to another office or partner.

#### Social profiles without embedded-post noise

The Actor recognizes profile links in HTML anchors and schema.org `sameAs` data. It excludes sharing links, reserved paths, embedded posts, and video URLs that do not identify a profile. When several profiles for one network appear, it prefers a profile whose handle resembles the domain or company name, then the most frequent one.

#### Transparent outcomes and result-based pricing

**The price is $0.60 per 1,000 results**, equivalent to $0.0006 per billed website. A result is one dataset row with `status: "ok"` and at least one extracted email, phone number, or supported social profile. Multiple contacts from the same website do not create additional result charges.

Rows with `no_contacts`, `blocked`, `js_only`, `not_found`, or `error` are delivered with `billed: false` and are free. You can inspect these rows instead of guessing why a website produced no contacts. Optional `proxy-page` events, when proxy fallback is available and enabled, are separate from the $0.60/1,000 result price.

You can also set a maximum cost per run on Apify. The Actor checks the remaining result budget before processing another website and stops cleanly when the limit is reached.

#### Lightweight runs with honest limitations

The fetcher uses direct HTTP with a Chrome-like TLS/HTTP2 fingerprint and does not start a browser. In a recorded 40-website run on an earlier beta build (0.1.1), it processed all 40 sites in 54.169 seconds and delivered 31 billable rows. A separate battery check processed four websites with four contact rows in 3.815 seconds. These are recorded examples, not a speed or yield guarantee; response times, blocking, page limits, and website content affect every run.

No browser also means no JavaScript rendering. Sites that publish useful contact details only after JavaScript execution can return `js_only`. Sites behind bot challenges or access controls can return `blocked`. These outcomes are not billed as results. The Actor does not promise to bypass every protection system or extract every address on every website.

### FAQ

#### What counts as one result?

One result is one website row with `status: "ok"`. The row must contain at least one email address, phone number, or supported social profile. Reading several pages or finding several contacts on that website still produces one result. The result price is $0.60 per 1,000 results.

#### Do I pay for websites with no contact details?

No. `no_contacts`, `blocked`, `js_only`, `not_found`, and `error` rows are not billed as results. They remain in the dataset with `billed: false` and a `statusReason` to help you audit the run. If you enable an available proxy fallback, separate proxy-page charges apply only to successfully fetched proxy pages associated with websites that finish as `ok`.

#### What input can I provide?

Use a list of full URLs such as `https://example.com/contact` or bare domains such as `example.com`. URLs without a scheme are treated as HTTPS. The Actor creates at most one row per normalized domain and skips duplicate domains, so `example.com` and `www.example.com` do not become separate websites. Invalid entries are skipped and recorded in the run summary.

#### Will it find every email address or phone number on a website?

No. It reads a bounded selection of pages, stays on the supplied website and relevant same-site subdomains, and extracts supported formats from HTML. Details on unvisited pages, inside images or documents, behind a login, or rendered only by JavaScript may be missed. Increasing `maxPagesPerDomain` can widen the search but cannot remove those limits.

#### Does the Actor guess or verify email addresses?

It does not generate likely addresses from employee names, and it does not perform mailbox or deliverability checks. It extracts addresses found in the pages it reads. Cleaned and normalized does not mean that an address is active, monitored, or suitable for a particular campaign.

#### What is the difference between generic and personal emails?

Generic addresses look like role or organizational mailboxes, such as `info@`, `contact@`, `billing@`, or a brand name. Personal addresses look like they belong to a named person. The classification uses local-part and domain rules, so edge cases can be mislabeled. Enable `onlyGenericEmails` if you want to discard addresses classified as personal.

#### Why does a row contain an email from another domain?

A page may mention an agency, reseller, partner, service provider, or another office. These addresses are retained by default and linked to their source page. Turn on `sameDomainEmailsOnly` to keep only addresses whose registrable domain matches the website. Phone numbers from other offices or partners can also appear; use `phoneSourcePages` to review them.

#### Does it work with JavaScript-only websites?

The Actor does not execute JavaScript. If the HTML contains almost no visible text and relies on scripts to render the site, the website can be labeled `js_only`. Some JavaScript-heavy sites still publish useful links or structured data in their HTML and can produce results, but full browser rendering is not provided.

#### Can it scrape sites protected by Cloudflare, CAPTCHA, or other bot controls?

Direct requests may be rejected or shown a challenge. The Actor recognizes common blocking responses and returns `blocked` instead of treating a challenge page as contact data. Optional proxy fallback retries a blocked page through the configured proxy when that feature is available. It can help on some sites, but recovery is not guaranteed, and separate proxy-page pricing may apply.

#### Why did my run finish as failed when the dataset contains rows?

A run fails if it delivers no `ok` result. It also fails when more than half of the processed websites are `blocked`, even if contact rows were found for the others. Those successful rows remain available in the dataset. This behavior prevents a mostly blocked or empty extraction from being reported as a successful run.

#### Can I export the data as JSON, CSV, or Excel?

Yes. Results are stored in the run's default Apify dataset, where you can use the standard dataset export formats or retrieve items through the API. Nested fields such as email types and source-page maps are most naturally represented in JSON; flattened tabular exports are useful when you mainly need domains, emails, phones, and profile links.

#### Where can I see errors and run totals?

Open the `RUN_SUMMARY` record in the run's default key-value store. It includes processed and billed counts, a breakdown by status, errors grouped by type, pages read, and invalid input entries. Individual rows also contain `status` and `statusReason` for website-level troubleshooting.

#### Is the output compliant with GDPR or other privacy rules?

The Actor does not certify legal compliance. Publicly available information can still be personal data, especially addresses classified as `personal`. You are responsible for having a valid basis for collection and use, honoring website terms and applicable privacy or marketing laws, and handling deletion or objection requests where required. Consider `onlyGenericEmails` when named-person addresses are unnecessary.

#### Does the Actor scrape social media platforms themselves?

No. It extracts links to social profiles from the websites it visits; it does not open LinkedIn, Facebook, Instagram, X/Twitter, YouTube, or TikTok to collect their internal data. Social links that exist only in embedded posts, videos, or unsupported JSON may not be detected.

### Settings and API

#### Input settings

| Setting | Default | What it controls |
| --- | --- | --- |
| `urls` | Required | Websites to scan. Accepts full URLs or bare domains; duplicate domains are skipped. |
| `maxItems` | `1000` | Maximum number of unique websites to process, from 1 to 1,000,000. |
| `maxPagesPerDomain` | `5` | Maximum pages read per website, from 1 to 20. Contact and legal pages are prioritized. |
| `onlyGenericEmails` | `false` | If `true`, removes addresses classified as `personal`. |
| `sameDomainEmailsOnly` | `false` | If `true`, removes email addresses on other registrable domains. |
| `maxConcurrency` | `20` | Number of websites processed in parallel, from 1 to 50. |
| `proxyConfiguration` | Disabled | Optional proxy fallback for blocked pages. Direct access is always tried first. If the Actor's pricing has no `proxy-page` event, this setting is ignored and a warning is logged. |

Example input:

```json
{
  "urls": [
    "https://apify.com",
    "https://buffer.com",
    "abcplumbing.com"
  ],
  "maxItems": 1000,
  "maxPagesPerDomain": 5,
  "onlyGenericEmails": false,
  "sameDomainEmailsOnly": false,
  "maxConcurrency": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

For API compatibility, `startUrls` is also accepted as a list of `{ "url": "https://example.com" }` objects, but `urls` is the recommended input shown in the Console.

#### Python API with a list of URLs

Install the Apify API client with `pip install apify-client`, create an Apify API token, and make it available as the `APIFY_TOKEN` environment variable. The following code starts the Actor, waits for completion, and reads every dataset row:

```python
import json
import os

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run = client.actor("kazkn/website-contact-details-scraper").call(
    run_input={
        "urls": [
            "https://apify.com",
            "https://buffer.com",
            "https://abcplumbing.com",
        ],
        "maxItems": 1000,
        "maxPagesPerDomain": 5,
        "onlyGenericEmails": False,
        "sameDomainEmailsOnly": False,
        "maxConcurrency": 20,
        "proxyConfiguration": {"useApifyProxy": False},
    }
)

items = client.dataset(run["defaultDatasetId"]).list_items().items
print(json.dumps(items, indent=2, ensure_ascii=False))
```

Use the run ID returned by the client to inspect the run in Apify. Dataset items contain the contact rows shown above, while the default key-value store contains `RUN_SUMMARY`. For scheduled or repeated workflows, keep a fresh input list for each run and use the source URLs and `scrapedAt` timestamps to assess when each contact was collected.

# Actor input Schema

## `urls` (type: `array`):

Websites to scan, one per line. A domain (stripe.com) or a full URL (https://www.stripe.com) both work. One result row per website.

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

Stop after this many websites (rows). Useful to cap a large list.

## `maxPagesPerDomain` (type: `integer`):

Home page plus the best contact, imprint / legal notice and about pages, read in that priority. 5 is enough for most sites.

## `onlyGenericEmails` (type: `boolean`):

Drop emails that look like a named person (jane.doe@...). Useful to limit personal data you collect.

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

Drop emails on other domains (agencies, sub-processors, partners mentioned on the page).

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

How many websites are scanned at the same time.

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

Optional. Pages are fetched directly; only a page that refuses access (HTTP 403 / bot check) is retried once through this proxy. Leave off unless many of your sites block. Each page that succeeds through the proxy is billed as a separate "proxy-page" event, only for websites where contact details were found.

## Actor input object example

```json
{
  "urls": [
    "https://apify.com",
    "https://buffer.com",
    "https://basecamp.com",
    "https://abcplumbing.com"
  ],
  "maxItems": 10,
  "maxPagesPerDomain": 5,
  "onlyGenericEmails": false,
  "sameDomainEmailsOnly": false,
  "maxConcurrency": 20,
  "proxyConfiguration": {
    "useApifyProxy": 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 = {
    "urls": [
        "https://apify.com",
        "https://buffer.com",
        "https://basecamp.com",
        "https://abcplumbing.com"
    ],
    "maxItems": 10,
    "maxPagesPerDomain": 5,
    "onlyGenericEmails": false,
    "sameDomainEmailsOnly": false,
    "maxConcurrency": 20,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("kazkn/website-contact-details-scraper").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 = {
    "urls": [
        "https://apify.com",
        "https://buffer.com",
        "https://basecamp.com",
        "https://abcplumbing.com",
    ],
    "maxItems": 10,
    "maxPagesPerDomain": 5,
    "onlyGenericEmails": False,
    "sameDomainEmailsOnly": False,
    "maxConcurrency": 20,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("kazkn/website-contact-details-scraper").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 '{
  "urls": [
    "https://apify.com",
    "https://buffer.com",
    "https://basecamp.com",
    "https://abcplumbing.com"
  ],
  "maxItems": 10,
  "maxPagesPerDomain": 5,
  "onlyGenericEmails": false,
  "sameDomainEmailsOnly": false,
  "maxConcurrency": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call kazkn/website-contact-details-scraper --silent --output-dataset

```

## MCP server setup

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

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/0vCgEWBI42HpdJPPF/builds/KDbZjytcjo2IkdAVt/openapi.json
