# Website Contact Scraper: Emails, Phones & WhatsApp (`laudable_instrument/contact-details-scraper-half-price`) Actor

Extract emails (MX-checked), phone numbers (E.164, validated) and 16 social/messaging profiles incl. WhatsApp & Telegram from any company website. One clean, de-duplicated row per site. Respects robots.txt. Free during launch: you pay only Apify platform usage.

- **URL**: https://apify.com/laudable_instrument/contact-details-scraper-half-price.md
- **Developed by:** [Unadhi](https://apify.com/laudable_instrument) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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, WhatsApp & Telegram at Half Price

Give it a list of company websites. For each site you get **one clean, de-duplicated row** with every public email, phone number and social or messaging profile we can find, plus the page each one was found on.

**Price: Free during launch: you pay only Apify platform usage.** You pay only for pages we actually fetch and read. Blocked, failed, robots.txt-disallowed and terms-of-service-blocked sites are free.

**Enricher mode:** point it at the dataset from any Apify run, for example a Google Maps scrape (`compass/crawler-google-places`), or paste a plain list of domains. It visits each business website once and copies `title`, `placeId`, `address` and other fields into `sourceItems`, so you can join the results back to your list.

**Privacy by default:** only shared **role mailboxes** (info@, sales@, support@, careers@, branch shops) are returned. Addresses that identify one person (ravi.k@, ceo@, hr.priya@) and staff LinkedIn profiles are **dropped and only counted** unless you switch them on (see "DPDP / GDPR notice" below).

### Why it's better

| | This Actor |
|---|---|
| Output | **1 row per website**, merged across all pages visited. No duplicate rows to clean up. |
| Duplicate inputs | `zoho.com`, `https://zoho.com/` and `www.zoho.com` count as one site, so you never pay twice |
| Emails | Checked against the domain's mail servers (free MX check → `mxValid`), labelled `role` (info@, sales@) or `personal`, and marked `sameDomain`. Template addresses like `yourname@…` and `you@yourbusiness.com` are dropped |
| Phones | Normalised to **E.164** (`+919848022338`) and checked for validity. Obvious placeholder numbers (`+1 234 567 8910`) are dropped. `tel:` links are preferred |
| WhatsApp | Click-to-chat links (`wa.me`, `api.whatsapp.com/send?phone=`) become real phone numbers with a ready `wa.me` link |
| Networks | LinkedIn (company pages; staff profiles go in a separate `linkedinPeople` field), X/Twitter, Facebook, Instagram, YouTube, TikTok, Pinterest, GitHub, Threads, **Telegram, WhatsApp**, Discord, Snapchat, Reddit |
| Crawl | Homepage first, then the most contact-like pages (contact, about, impressum, support). Usually 3–4 pages per site are enough |
| Politeness and legality | Follows robots.txt the RFC 9309 way, uses an honest bot User-Agent, and **reads each site's terms of service**. Sites whose terms forbid scraping/crawling, and platform pages (Facebook, Instagram, LinkedIn, marketplaces) that Maps often lists as a "website", are skipped for free. No proxies, no logins, no bypassing blocks |
| Enricher | Input from an Apify dataset ID (auto-detects `website` / `url` / `domain` fields; Google Maps links are ignored), a plain domain list, or start URLs. Branches that share one website are crawled and charged once |
| Spend limit | Pages are reserved before crawling, so the run stops cleanly at your max-charge limit instead of doing unpaid work |

### Input

Enrich a Google Maps (or any) dataset:

```json
{ "datasetId": "<dataset ID of your Google Maps run>", "maxPagesPerSite": 4, "defaultCountry": "IN" }
```

Or a list of sites:

```json
{
  "urls": ["kimshospitals.com", "https://www.fmsdental.com"],
  "startUrls": [{ "url": "https://www.zoho.com" }, { "url": "basecamp.com" }],
  "maxPagesPerSite": 4,
  "checkEmailMx": true,
  "defaultCountry": "IN"
}
```

### Output (one row per site, shortened)

```json
{
  "domain": "tanishq.co.in",
  "status": "ok",
  "pagesScraped": 4,
  "emails": [],
  "phones": [
    { "phone": "+9118002660123", "source": "tel-link", "valid": true, "country": "IN" },
    { "phone": "+918147349242", "source": "text", "whatsapp": "https://wa.me/918147349242" }
  ],
  "socials": { "instagram": [{ "url": "https://www.instagram.com/tanishqjewellery", "foundOn": "https://www.tanishq.co.in" }],
               "whatsapp": [{ "url": "https://api.whatsapp.com/send?phone=8147349242", "foundOn": "https://www.tanishq.co.in" }] },
  "counts": { "emails": 0, "phones": 3, "socialProfiles": 4, "whatsapp": 1, "telegram": 0 }
}
```

`status` is one of `ok`, `blocked-by-robots` (the site asks bots not to crawl), `blocked-by-terms` (the site's terms forbid scraping; the clause is quoted in `error`), `blocked-by-tos-list` (platform page such as Facebook), `blocked-by-site` (HTTP 401/403/429), `rate-limited` or `failed`. None of the non-`ok` statuses cost anything. Rows also carry `personalEmailsDropped`, `personProfilesDropped` and, in enricher mode, `sourceItems`.

### Measured on 20 real company websites (29 Sep 2026)

- 14 of 20 sites returned data. That's every site that allowed crawling: 14 of 14, with 0 errors. 2 sites disallow all bots in robots.txt and 4 block automated clients (HTTP 403). All 6 were reported and not charged.
- Of those 14: at least one email on 10, a phone on 6, a social profile on 13, WhatsApp on 2. In total 18 emails (all passed the MX check), 13 phones and 67 company social profiles.
- 3.8 pages per site on average. The median site took 1.7 s at 10 sites in parallel.

### Measured in enricher mode (29 Sep 2026, a 31-item Google-Maps-format dataset of Hyderabad businesses)

- 31 items → 29 with a website → **27 unique sites** (2 branches shared a website, so they were charged once).
- 20 sites returned data. We skipped Facebook and Instagram "websites" (2), 1 site whose terms forbid harvesting, 2 sites that answered 403 to our bot and 2 that were unreachable, all for free.
- 65 pages → 32 role emails on 10 sites, 171 phone numbers on 15 sites and 101 social profiles. **18 personal emails were dropped** by the privacy default.

### DPDP / GDPR notice (please read)

- **Work emails and phone numbers of identifiable people are personal data** under India's Digital Personal Data Protection Act 2023 and the EU/UK GDPR. The DPDP exemption for publicly available data covers only data **the person themself made public**; "it is on a website" is not automatically enough.
- That is why the defaults are `includePersonalEmails: false` and `includePersonProfiles: false`. Turn them on **only** if you have a lawful basis (e.g. a documented legitimate-interest assessment under GDPR), give people notice, honour opt-out/erasure requests promptly, and keep the `foundOn` source URL of every contact.
- Role mailboxes can still reach a person. Keep outreach relevant, identify yourself, include an opt-out, and follow CAN-SPAM, TRAI (no unsolicited commercial SMS/calls in India) and WhatsApp's own policies.
- You, as the user who runs the Actor and decides what to do with the output, are responsible for compliance. This notice is general information, **not legal advice**.
- The Actor never logs in, never bypasses blocks and never collects data behind a login.

### Pricing

Free during launch: you pay only Apify platform usage.

# Actor input Schema

## `startUrls` (type: `array`):

Company websites (homepages). Duplicates of the same domain are removed automatically, so you never pay twice. You can also use the plain list below or a dataset.

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

One per line, e.g. zoho.com or https://www.kimshospitals.com. Merged with the list above.

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

ID of a dataset from another run, e.g. a Google Maps scrape (compass/crawler-google-places). The Actor reads each item's website field, visits each unique site once, and copies identifying fields (title, placeId, address...) into sourceItems so you can join the results back.

## `datasetUrlField` (type: `string`):

Leave empty to auto-detect (website, websiteUrl, url, domain, homepage...). Google Maps URLs are ignored because they are not the business website.

## `datasetCarryFields` (type: `array`):

Copied into sourceItems on the output row.

## `maxDatasetItems` (type: `integer`):

0 = all.

## `includePersonalEmails` (type: `boolean`):

Off: only shared role mailboxes (info@, sales@, support@, careers@...) are returned; addresses that identify one person (ravi.k@, ceo@, hr.priya@) are dropped and only counted. Turn on only if you have a lawful basis under the DPDP Act 2023 / GDPR and will honour opt-outs.

## `includePersonProfiles` (type: `boolean`):

Off: personal LinkedIn /in/ profiles found on the site are dropped (company pages are always kept).

## `respectTermsOfService` (type: `boolean`):

Skip sites on our platform blocklist (Facebook, Instagram, LinkedIn, marketplaces...) and sites whose terms prohibit scraping/crawling/automated access. Skipped sites are not charged.

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

Homepage + the most contact-like pages (contact, about, impressum, support…). 3–5 is enough for most sites. You pay only for pages actually fetched.

## `checkEmailMx` (type: `boolean`):

Free DNS check that the email's domain can receive mail. Adds mxValid to each email.

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

ISO country code used to read local-format numbers when the site's domain doesn't imply one (e.g. US, IN, GB).

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

Skip pages the site disallows for bots. Recommended.

## `followLinks` (type: `boolean`):

By default only links on the homepage are followed.

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

Number of websites processed at the same time.

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

Give up on a page request after this many seconds.

## `maxRetries` (type: `integer`):

How many times a failed page request is retried.

## `includePagesVisited` (type: `boolean`):

Add the list of URLs that were fetched for each site to the output row.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.zoho.com"
    },
    {
      "url": "https://basecamp.com"
    }
  ],
  "urls": [],
  "datasetCarryFields": [
    "title",
    "placeId",
    "categoryName",
    "address",
    "city",
    "countryCode"
  ],
  "maxDatasetItems": 0,
  "includePersonalEmails": false,
  "includePersonProfiles": false,
  "respectTermsOfService": true,
  "maxPagesPerSite": 4,
  "checkEmailMx": true,
  "defaultCountry": "US",
  "respectRobotsTxt": true,
  "followLinks": false,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 15,
  "maxRetries": 2,
  "includePagesVisited": true
}
```

# Actor output Schema

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

No description

## `runStats` (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 = {
    "startUrls": [
        {
            "url": "https://www.zoho.com"
        },
        {
            "url": "https://basecamp.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("laudable_instrument/contact-details-scraper-half-price").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 = { "startUrls": [
        { "url": "https://www.zoho.com" },
        { "url": "https://basecamp.com" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("laudable_instrument/contact-details-scraper-half-price").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 '{
  "startUrls": [
    {
      "url": "https://www.zoho.com"
    },
    {
      "url": "https://basecamp.com"
    }
  ]
}' |
apify call laudable_instrument/contact-details-scraper-half-price --silent --output-dataset

```

## MCP server setup

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

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/xd5sa0Cqni371sLi5/builds/d9Ww4QMiphfWTA8n4/openapi.json
