# Website Email Phone Contacts Scraper (`pradio/website-email-phone`) Actor

Website email phone contacts scraper reads your list of URLs and returns each site's emails, phones, socials, forms, contact\_page\_url, field\_provenance and status on every row.

- **URL**: https://apify.com/pradio/website-email-phone.md
- **Developed by:** [Pradio Actors](https://apify.com/pradio) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.36 / 1,000 page returneds

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

## Website Email Phone Contacts Scraper

### What does Website Email Phone Contacts Scraper do?

Website Email Phone Contacts Scraper reads the contact pages on your list of URLs and returns one row per site. Each row carries the emails, phone numbers, social profiles and contact forms the site publishes. Per-field provenance names the page behind every value. Paste the list and press Start. Each site lands in your dataset as it is read. A site that yields nothing costs nothing. Its row says why.

In a measured run over 116 sites the Actor had never visited, 49% returned contact details over plain HTTP. No login and no browser are needed. Each data row carries 33 fields. An empty cell is an answer rather than a shrug. `status` names the verdict. `field_provenance` names where each value came from. A returned page costs $0.0016.

### Who uses Website Email Phone Contacts Scraper

| Who | What they run it for |
|---|---|
| Lead-list builders | Turn a list of prospect domains into email and phone columns for a campaign. |
| Lead-gen agencies | Hand clients a contact sheet where every miss carries its reason. |
| Sales-ops teams | Add direct contact points to account lists before an outreach push. |
| Solo founders | Get a niche contact list without maintaining a personal script. |

### Features

- **Contact page discovery.** Reads the page you give it. It then follows the contact, about, imprint, team and support links the site itself offers. When no link is offered it tries the common paths. `/contact` and `/about` are among them.
- **Deobfuscation built in.** It decodes masked `data-cfemail` addresses and `name [at] site [dot] com` substitutions. It also decodes emails assembled inside JavaScript or base64 strings.
- **Phones split by confidence.** `tel:` links, structured phone keys and phone-named attributes land in `phones`. Digit strings that pass the shape check join them. The rest go to `phonesUncertain` for review instead of being silently trusted.
- **Thirteen social networks.** Links are lifted from each page: LinkedIn, X (Twitter), Instagram, Facebook, YouTube, TikTok and Pinterest. Also Discord, Snapchat, Threads, Telegram, Reddit and WhatsApp.
- **Provenance on every value.** `field_provenance` names the page behind each email, phone and profile on the row, and the reading method that read it.
- **Contact forms captured.** When a site offers a form instead of an address, `form_action_url` carries where that form posts. The site is still reachable.
- **One row per site, or one per page.** `mergeContacts` folds every page a site answers into a single row. Switched off, each fetched page is its own row with its own verdict.
- **Robots.txt honoured per host.** A path the site disallows is never requested. A refused read ends that site as `blocked` rather than being pushed past.
- **A second arm for a dead address.** A site that fails at the network level gets one retry. The other form of the host is tried: apex to `www` or the reverse. Only then is it called a miss.
- **Proxy support.** Runs through Apify Proxy by default and accepts your own proxy URLs.
- **Crawl controls.** `maxDepth`, `maxRequestsPerStartUrl`, `maxRequests`, `sameDomain` and `considerChildFrames` bound how deep each site is read and how its rows are shaped.

### What you can count on

- You pay only for rows whose status is `found`. A site the run cannot answer comes back as an uncharged `ITEM_STATUS` row. It names the entry and the verdict.
- Every row is charged only after it is written to your dataset. A row you cannot see is never billed.
- A run that finds nothing returns one `NO_CONTACTS_FOUND` row that says so. It is never an empty dataset.
- A spending limit stops the run cleanly. A `STOPPED_EARLY` row says how many rows came back and how many were left out.
- Every run writes a `RUN_SUMMARY` entry to the key-value store: `rowsFetched`, `rowsPushed`, `rowsCharged` and `duplicatesDropped`, so a short run and a broken one are told apart.
- If the run's own machinery changes what it answers, the run fails with the error in the log. A fault past the per-site boundary is not one site's miss, and a partial set is never returned as though it were whole.
- No value is invented. A field the page does not show stays empty. This page says which fields fill rarely.

### Why this one

The most-used alternative on this platform was run on one of the three sites on the prefilled list, the carbonvisuals.com contact page, on 2026-09-20. Each Actor returned one row for it. What the two rows showed:

- Its `phones` column was empty. The number sat in `phonesUncertain` beside four strings that are not phones. This Actor filed `+44 (0)7973 641131` under `phones`, where a dialable number belongs.
- Its row names no page per value. The pages it read sit in one `scrapedUrls` list, and it carries no `url` or `referrerUrl`. Every value here names its page and carrier in `field_provenance`.
- It reported a Twitter profile the site never publishes, assembled from an unrelated Flickr account's screen name. Its `youtubes` counted embedded video players as channel links. This Actor does not infer handles. A field the page does not publish stays empty.
- The links it did file sat under the bare-plural names. Its canonical `Profiles` columns are its paid enrichment, off by default and empty. This Actor fills the bare plurals and the canonical forms beside them.
- Its price is $0.002 a row. It adds a $0.001 start fee on every run. This Actor charges $0.0016 for a returned page and nothing for a miss. It adds no start fee beyond Apify's platform charge. Start fees billed on this shelf run from $0.00005 to $0.001 a run.

On 116 sites the Actor had never visited, 49% returned contact details. The measurement ran without a browser, so the figure is conservative. Every miss came back a named verdict. 23 `blocked`, 16 `fetch_failed`, 10 `js_required`, 6 `none_published` and 4 `form_only`.

### What data does Website Email Phone Contacts Scraper return?

One row per site on your list. With `mergeContacts` off, one row per fetched page instead. This is a real row, abbreviated, from the prefilled example:

```json
{
  "originalStartUrl": "https://www.cas.org/contact",
  "domain": "cas.org",
  "url": "https://www.cas.org/pt-br/contact",
  "referrerUrl": "https://www.cas.org/contact",
  "emails": ["help@cas.org", "apachelp@acs-i.org", "china@acs-i.org", "jsingh@acs-i.org", "cisobe@acs-i.org", "EMEAhelp@cas.org", "cas-pr@cas.org"],
  "phones": ["+18007534227", "+16144473731", "+65-3125-2705", "+61-2-8550-2646", "+82-70-4732-6766", "+86-10-62508026/7", "+81-3-5456-5696", "+49-721-95-883-155", "+86-10-62508026"],
  "phonesUncertain": ["117260204", "100-6162"],
  "linkedIns": ["https://www.linkedin.com/company/cas"],
  "facebooks": ["https://www.facebook.com/CAS"],
  "facebookProfiles": ["https://facebook.com/CAS"],
  "twitters": ["https://www.twitter.com/CASChemistry"],
  "twitterProfiles": ["https://twitter.com/CASChemistry"],
  "youtubes": ["https://www.youtube.com/user/ChemAbstractsService"],
  "youtubeProfiles": ["https://youtube.com/user/ChemAbstractsService"],
  "contact_page_url": "https://www.cas.org/pt-br/contact",
  "extraction_method": "mailto+tel+json-ld+js-data+text+href",
  "field_provenance": {
    "emails": [
      { "value": "help@cas.org", "page": "https://www.cas.org/contact", "method": "mailto" },
      { "value": "apachelp@acs-i.org", "page": "https://www.cas.org/contact", "method": "mailto" },
      { "value": "china@acs-i.org", "page": "https://www.cas.org/contact", "method": "mailto" },
      { "value": "jsingh@acs-i.org", "page": "https://www.cas.org/contact", "method": "mailto" },
      { "value": "cisobe@acs-i.org", "page": "https://www.cas.org/contact", "method": "mailto" },
      { "value": "EMEAhelp@cas.org", "page": "https://www.cas.org/contact", "method": "mailto" },
      { "value": "cas-pr@cas.org", "page": "https://www.cas.org/contact", "method": "mailto" }
    ],
    "phones": [
      { "value": "+18007534227", "page": "https://www.cas.org/contact", "method": "tel" },
      { "value": "+16144473731", "page": "https://www.cas.org/contact", "method": "tel" },
      { "value": "+65-3125-2705", "page": "https://www.cas.org/contact", "method": "tel" },
      { "value": "+61-2-8550-2646", "page": "https://www.cas.org/contact", "method": "tel" },
      { "value": "+82-70-4732-6766", "page": "https://www.cas.org/contact", "method": "tel" },
      { "value": "+86-10-62508026/7", "page": "https://www.cas.org/contact", "method": "tel" },
      { "value": "+81-3-5456-5696", "page": "https://www.cas.org/contact", "method": "tel" },
      { "value": "+49-721-95-883-155", "page": "https://www.cas.org/contact", "method": "tel" },
      { "value": "+86-10-62508026", "page": "https://www.cas.org/contact", "method": "json-ld" }
    ],
    "phonesUncertain": [
      { "value": "117260204", "page": "https://www.cas.org/contact", "method": "js-data" },
      { "value": "100-6162", "page": "https://www.cas.org/contact", "method": "text" }
    ],
    "linkedIns": [
      { "value": "https://www.linkedin.com/company/cas", "page": "https://www.cas.org/contact", "method": "href" }
    ],
    "twitters": [
      { "value": "https://www.twitter.com/CASChemistry", "page": "https://www.cas.org/contact", "method": "href" }
    ],
    "instagrams": [],
    "facebooks": [
      { "value": "https://www.facebook.com/CAS", "page": "https://www.cas.org/contact", "method": "href" }
    ],
    "youtubes": [
      { "value": "https://www.youtube.com/user/ChemAbstractsService", "page": "https://www.cas.org/contact", "method": "href" }
    ],
    "tiktoks": [],
    "pinterests": [],
    "discords": [],
    "snapchats": [],
    "threads": [],
    "telegrams": [],
    "reddits": [],
    "whatsapps": []
  },
  "form_action_url": "https://www.cas.org/contact",
  "obfuscated": false,
  "depth": 1,
  "status": "found",
  "row_type": "ROW"
}
```

#### Contact columns

| Field | Type | What it carries and where it is read |
|---|---|---|
| `emails` | array | Email addresses read from the site: page text, `mailto:` links and decoded obfuscated forms. |
| `phones` | array | Phone numbers carried by `tel:` links, structured data or canonical phone-shaped text. |
| `phonesUncertain` | array | Digit strings that could be phone numbers but carry no phone marker; glance before dialling. |
| `form_action_url` | string | The action URL of the contact form the site publishes, when there is one. |

#### Social columns

Each page's outbound profile links land in two spellings. The bare plural keeps the link as the site published it. The five `Profiles` fields carry the canonical form. Scheme- and www-normalised, `x.com` folded to `twitter.com`, duplicates collapsed.

| Field | What it carries |
|---|---|
| `linkedIns` | LinkedIn company or profile links the site publishes, as published. |
| `twitters` | X (Twitter) links the site publishes, as published; `twitterProfiles` carries the canonical form. |
| `instagrams` | Instagram links the site publishes, as published; `instagramProfiles` carries the canonical form. |
| `facebooks` | Facebook page links the site publishes, as published; `facebookProfiles` carries the canonical form. |
| `youtubes` | YouTube channel links the site publishes, as published; `youtubeProfiles` carries the canonical form. |
| `tiktoks` | TikTok profile links the site publishes, as published; `tiktokProfiles` carries the canonical form. |
| `pinterests` | Pinterest profile links the site publishes, as published. |
| `discords` | Discord server invite links the site publishes. |
| `snapchats` | Snapchat profile links the site publishes. |
| `threads` | Threads profile links the site publishes. |
| `telegrams` | Telegram channel or user links the site publishes. |
| `reddits` | Reddit community or profile links the site publishes. |
| `whatsapps` | WhatsApp contact links (`wa.me`) the site publishes. |
| `facebookProfiles` | Canonical Facebook page URLs: scheme- and www-normalised, duplicates collapsed. |
| `instagramProfiles` | Canonical Instagram profile URLs: scheme- and www-normalised, duplicates collapsed. |
| `youtubeProfiles` | Canonical YouTube channel URLs: scheme- and www-normalised, duplicates collapsed. |
| `tiktokProfiles` | Canonical TikTok profile URLs: scheme- and www-normalised, duplicates collapsed. |
| `twitterProfiles` | Canonical X (Twitter) profile URLs: scheme- and www-normalised, `x.com` folded to `twitter.com`, duplicates collapsed. |

#### Where the row came from

| Field | Type | What it carries |
|---|---|---|
| `originalStartUrl` | string | The start URL this row came from: your input entry. Exact duplicates on it are dropped before charging. |
| `domain` | string | The site you named in the input; a row keys back to your list through this even when the site redirects elsewhere. |
| `url` | string | The page this row was read from. |
| `referrerUrl` | string | The page that linked to this row. |
| `depth` | number | The link depth this row was reached at: a URL you gave is depth zero, a page it links to is one. |
| `contact_page_url` | string | The contact page the site itself publishes, when one was found. |

#### Evidence columns

| Field | Type | What it carries |
|---|---|---|
| `extraction_method` | string | Which readers produced this row: text, `mailto:`/`tel:` links, JSON-LD, data attributes, same-host frames, decoded obfuscation. |
| `field_provenance` | object | Per field, where each value was read: the page and the carrier (link, text, structured data). |
| `obfuscated` | boolean | True when the page hides its contacts behind obfuscation: masked emails, `[at]`/`[dot]` spellings or JavaScript-assembled addresses. |

#### Status columns

| Field | Type | What it carries |
|---|---|---|
| `status` | string | `found` on a data row; on an `ITEM_STATUS` row it is the miss verdict. |
| `row_type` | string | `ROW`, `ITEM_STATUS`, `NO_CONTACTS_FOUND` or `STOPPED_EARLY`. See Output below. |
| `reason` | string | Status rows only: why the run returned no rows, or why it stopped early. |
| `rowsFetched` | integer | Status rows only: how many rows the source handed over before de-duplication and the cap. |
| `rowsReturned` | integer | Status rows only: how many data rows are in the dataset (0 on an empty result; the count before the charge limit on `STOPPED_EARLY`). |
| `rowsRemaining` | integer | Status rows only: how many fetched rows were not returned (0 on an empty result). |

How often fields fill, measured on the 57 sites that resolved out of the 116-site run. The row-level columns fill on every resolved row. That is `url`, `domain`, `originalStartUrl`, `status`, `extraction_method`, `field_provenance`, `obfuscated` and `depth`. `contact_page_url` and `referrerUrl` filled on 91.2%, `form_action_url` on 75.4%. In the contact columns, `phonesUncertain` filled on 91.2%, `phones` on 80.7% and `emails` on 64.9%. In the social columns, `facebooks` and `facebookProfiles` filled on 78.9%, `instagrams` and `instagramProfiles` on 63.2%, `twitters` and `twitterProfiles` on 59.6%, `youtubes` and `youtubeProfiles` on 61.4%, and `linkedIns` on 57.9%. `tiktoks` and `tiktokProfiles` and `pinterests` filled on 14%, `discords` and `threads` on 3.5%. `snapchats`, `telegrams`, `reddits` and `whatsapps` filled on no resolved row. An empty array means the fetched pages carried no such link. It does not mean the read failed. Outside the four niche networks above and the status-only fields, every declared field filled on at least one captured row. The dataset's Overview view shows the headline contact columns. Switch to the All fields view for every column above.

### How much does it cost?

Pricing for this Actor is pay per event: **$0.0016 per returned page**, the `page-returned` event on your invoice. One charge for each row of contact details written to your dataset. The charge lands only after the row exists. Status rows, dropped duplicates and rows past your spending limit are never charged. With `mergeContacts` on, a site folds into one row. It costs one charge no matter how many of its pages were read.

Apify also bills its own start charge on every run, the `apify-actor-start` event at $0.00005 per gigabyte of memory allocated, a fraction of a cent. This Actor adds no start fee of its own.

What a list costs at the measured 49% fill:

| Sites you paste | Sites expected to answer | You pay | If every site answers |
|---|---|---|---|
| 100 | about 49 | about $0.08 + the start charge | $0.16 + the start charge |
| 1,000 | about 491 | about $0.79 + the start charge | $1.6 + the start charge |
| 10,000 | about 4,914 | about $7.86 + the start charge | $16 + the start charge |

In list terms: paste 100 URLs and roughly 49 sites answer. The run costs about $0.08 plus the start charge. Every miss is a free status row. Paste 1,000 URLs and roughly 491 answer, about $0.79 plus the start charge. A run where nothing answers produces one `NO_CONTACTS_FOUND` row. It costs the start charge alone.

To spend less, keep `maxItems` low while testing. Split a big list into smaller runs. Set a spending limit in the Console before a long run. A limit ends the run with a `STOPPED_EARLY` row, not a surprise bill.

### How do I use Website Email Phone Contacts Scraper?

1. Open this Actor's Store page and press **Start** (or **Try for free**).
2. Paste your websites into **Start URLs**, one entry per site. The three prefilled entries are a working example.
3. Press **Start**. Rows appear in the **Dataset** tab as each site is read.
4. Export the dataset as JSON, CSV or Excel, or read it through the API.

A minimal input looks like this:

```json
{
  "startUrls": [
    { "url": "https://www.carbonvisuals.com/contact" },
    { "url": "https://www.cas.org/contact" },
    { "url": "https://www.acxiom.com/contact-us/" }
  ],
  "proxyConfig": { "useApifyProxy": true },
  "maxItems": 100
}
```

The same run through the Apify API:

```bash
curl -X POST "https://api.apify.com/v2/acts/Pradio~website-email-phone/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls":[{"url":"https://www.cas.org/contact"}],"proxyConfig":{"useApifyProxy":true}}'
```

### Input

| Input | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array | three example sites | The websites to read, one entry per site. Required. |
| `proxyConfig` | object | `{ "useApifyProxy": true }` | Where requests come from. Required; Apify Proxy by default. |
| `maxItems` | integer | 100 | The most rows one run returns. The run stops there and says so. |
| `maxDepth` | integer | 1 | How far to follow the site's own links looking for contact pages. 1 reads the start page plus pages it links to. |
| `maxRequestsPerStartUrl` | integer | 8 | The most pages fetched for one site, counting the start page and the contact pages it leads to. |
| `maxRequests` | integer | no cap | A fetch budget shared across the whole run; leave unset for no cap. |
| `sameDomain` | boolean | on | Only follow links on the site's own host. Off lets contact links pointing elsewhere be followed too. |
| `mergeContacts` | boolean | on | Fold every fetched page of a site into one row. Off gives one row per fetched page. |
| `considerChildFrames` | boolean | off | Also read same-host embedded frames, contacts living inside an iframe on the page, up to three per page. |
| `scrapeSocialMediaProfiles` | object | all networks on | Per-network switches. A network set to false is not extracted; unset reads them all. |

#### startUrls

Every distinct entry produces a row. A bare domain, a full URL or a contact page deep-link all work. A missing scheme is assumed to be `https`. An entry that cannot be parsed comes back as a `bad_url` status row naming it. It is never silently dropped. If a site's address fails at the network level, the other form of the host is tried once. Apex becomes `www`, or `www` becomes apex. Only then is the site called a miss.

#### mergeContacts

On by default. Every page fetched for a site folds into one row. A site is one line in your export and one charge. Set it off and each fetched page becomes its own row with its own verdict. That is useful when you want to see which page of a site carries the contacts.

#### scrapeSocialMediaProfiles

All thirteen networks are collected by default. Set any single network to `false` to skip it, for example `{ "tiktoks": false }`. Or pass `{ "enabled": false }` to skip social links entirely.

#### proxyConfig

Requests go through Apify Proxy by default. Residential proxies help where a site quietly drops or throttles datacenter traffic without an explicit refusal. Supply your own `proxyUrls` to send requests through your own addresses. Or set `useApifyProxy` to false for direct fetching. The proxy spreads load. It is never used to push past a refusal. A `401`, `403` or `429` ends the site as `blocked` either way.

### Output

Every row carries `row_type`, which kind of line it is:

| `row_type` | What it means |
|---|---|
| `ROW` | A data row, billed after it is written to the dataset. |
| `ITEM_STATUS` | A per-site miss: the full row shape with `status` naming the verdict, pushed so you see it, never charged. |
| `NO_CONTACTS_FOUND` | One row for a run where nothing answered; `reason` says so. Never charged. |
| `STOPPED_EARLY` | One row when the charge limit ended the run, carrying `rowsReturned` and `rowsRemaining`. Never charged. |

The `status` verdicts a row can carry:

| `status` | What it means |
|---|---|
| `found` | The row carries contact details: an email or a phone. This is the billed verdict. |
| `armored` | The page hides its contacts behind obfuscation that did not decode. |
| `form_only` | The site offers a contact form and nothing else; `form_action_url` says where it posts. |
| `none_published` | The pages answered but publish no email, phone or contact form. A `none_published` row can still carry social links it found, since `found` asks for an email or a phone. |
| `fetch_failed` | The site did not answer: a network-level miss on both host arms, an HTTP error that is not a refusal, or a page that gave no answer in 20 seconds. |
| `blocked` | The site refused the read: a `401`, `403` or `429`, or a robots.txt disallow. Never retried past. |
| `bad_url` | The entry could not be parsed as a URL. |
| `js_required` | Every fetched page answered a thin JavaScript shell with no visible rendered text. A shell's markup can still carry links and embedded strings, and those land on the row. |

The declared vocabulary also covers `not_found`, `bad_input`, `timeout`, `aborted`, `error` and `failed` for misses of those shapes. This Actor's misses land on the verdicts above. Anything other than `found` is an uncharged `ITEM_STATUS` row.

Here is a real miss mix from the measured run over 116 sites the Actor had never visited. 57 sites returned contact details. 23 came back `blocked`, 16 `fetch_failed`, 10 `js_required`, 6 `none_published` and 4 `form_only`. That is the shape a first run on a fresh list usually takes. Just under half answer, and the rest name their reason.

Every run also writes `RUN_SUMMARY` in the default key-value store. Its fields are `rowsFetched`, `rowsPushed`, `rowsCharged`, `rowsUncharged`, `duplicatesDropped` and `stoppedEarly`. As a worked example, paste 50 URLs and suppose 47 sites answer with contacts. The dataset holds 47 billed data rows and 3 uncharged status rows. `RUN_SUMMARY` shows 50 fetched. A zero result means the source answered and had nothing. It is one status row, not an empty dataset and not an error.

### What can you do with the data?

A lead-list builder pastes a list of 100 prospect domains. She exports the dataset to CSV and loads `emails` and `phones` straight into the campaign sheet. The `ITEM_STATUS` rows tell her which misses were `blocked` and which simply publish nothing. She stops re-checking the dead ends by hand.

An agency researcher runs a client's prospect list before a pitch. `field_provenance` lets him cite the exact page behind every email and phone number. That answers the client who asks where the data came from.

A sales-ops person enriches an account list before an outreach push. `phones` feeds the dialer. `phonesUncertain` is flagged for a human glance. `obfuscated` marks the sites that hide their contacts, worth a manual look.

A founder building a partner list filters `status` for `found`. She then opens `form_action_url` on the `form_only` sites and uses their contact forms directly. Nothing on the list is a mystery. Every site either produced a value or named why it did not.

### Use Website Email Phone Contacts Scraper with AI agents

```
claude mcp add --transport http apify "https://mcp.apify.com?tools=Pradio/website-email-phone"
```

One line registers this Actor with the Apify MCP server. Your agent can then run it and read the rows it returns.

### Personal data

Each row is the email addresses, phone numbers and social profile links that one page you named displays. Those details can identify private individuals, not only businesses. Data protection law can apply to what your run collects.

You are the controller of the data your run collects. That means informing data subjects who you are and which page their details came from. It means telling them what you will use the data for and what their rights are. It also means honouring objection and deletion requests.

Anyone can ask for the exclusion of their page and the deletion of their rows. Open an issue on this Actor's Issues tab and the request will be honoured.

The Actor's own conduct stays narrow. Logged-out reads only, one page per request until the cap. A robots.txt disallow or a `403` ends the read of that page as `blocked`. No retry past a refusal. No login or credential. No wall defeated. No proxy used to bypass one. Only the contact points a page itself displays are collected. There is no enrichment, inference or aggregation across pages beyond the list you named. `maxItems` defaults to 100 rows per run.

### Release notes

- **0.1.9** (2026-09-20): first public version.

### Limits

- **Server-rendered HTML only.** Pages that render their content only under a browser answer a thin script shell. They come back `js_required`. The Actor does not bundle a renderer.
- **Refusals are respected.** A `401`, `403` or `429`, or a robots.txt disallow, ends that site as `blocked`. It is not retried past and not routed around.
- **The per-site crawl is shallow.** At most `maxDepth` hops off the start page (default one) and `maxRequestsPerStartUrl` pages per site. Fetches on one site are paced. A site that buries its contacts deeper is a miss, not an exception.
- **Niche networks stay empty on most rows.** `tiktoks` and `pinterests` filled on 14% of resolved rows, `discords` and `threads` on 3.5%. `snapchats`, `telegrams`, `reddits` and `whatsapps` filled on no resolved row. An empty array is the honest answer when the pages carry no such link.
- **`phonesUncertain` is a review pile, not a dial list.** It holds digit strings with no phone marker. Dates or IDs can land in it. Look before you load it anywhere.
- **Very large pages are read in part.** Only the first portion of a fetched page is read. A heavier page's tail is not seen.
- **One request waits twenty seconds.** A page that does not answer in time ends the site as `fetch_failed`.
- **Errors run toward visible misses.** A site that fails becomes a status row. A value that is not there stays empty.
- **`maxItems` caps a run at 100 rows by default.** Raise it for bigger lists. Or set a spending limit and read `STOPPED_EARLY` when it binds.

### Troubleshooting

**I pasted a list of URLs but got back fewer data rows.** Every distinct entry produces a row. Misses come back as `ITEM_STATUS` rows naming the entry and the verdict. `blocked`, `js_required`, `fetch_failed` and the rest. They are never billed. Exact duplicate entries are dropped before charging and counted as `duplicatesDropped` in `RUN_SUMMARY`.

**My run returned a single `NO_CONTACTS_FOUND` row.** Every site missed. The `reason` field says the source answered and had nothing. Nothing was billed beyond the start charge. Check that the URLs load for you in a browser first.

**A site came back `js_required`.** Its pages answered a JavaScript shell with no visible text. The Actor reads server-rendered HTML over plain HTTP and does not bundle a browser.

**My `phonesUncertain` column contains dates and ID-shaped numbers.** That column is the review pile by design. Anything a `tel:` link or a structured field did not vouch for lands there instead of in `phones`. Confident numbers are not diluted, and the maybes are not lost.

**The run ended with a `STOPPED_EARLY` row.** Your spending limit was reached. `rowsReturned` and `rowsRemaining` on that row say how much came back and how much was left. Raise the limit and re-run to see the rest.

Something else off? Open an issue on this Actor's **Issues** tab.

### FAQ

**Can I use integrations with Website Email Phone Contacts Scraper?**
Yes. It is an Apify Actor. Schedules, webhooks and Apify's integrations for tools like Zapier, Make and Google Sheets all work with it. The dataset exports as JSON, CSV, Excel or XML.

**Can I use Website Email Phone Contacts Scraper with the Apify API?**
Yes. The `curl` example above starts a run and returns the dataset items. Runs, datasets and key-value store entries are all reachable through the API.

**Can I use Website Email Phone Contacts Scraper through an MCP server?**
Yes. Register the Apify MCP server with the line in the AI-agents section. This Actor becomes a tool your agent can call.

**Is it legal to scrape websites for contact details?**
This Actor reads only pages the sites themselves publish. It honours each host's robots.txt, stops on a refusal, and never logs in or works around a block. Whether collecting the data fits your purpose depends on your jurisdiction and use. The rows can identify private individuals. Read the Personal data section first.

### See also

- [Imprint Contact Scraper](https://apify.com/pradio/imprint-contact): reads the legal and imprint pages on company websites. It returns the contact details the law requires them to publish. The regulated counterpart of this Actor, same row shape.
- [Broken Link Checker](https://apify.com/pradio/broken-link): scans the same sites for dead links and broken pages. A site-health complement that reads a URL list the same way.

### Not affiliated

Website Email Phone Contacts Scraper is an unofficial tool. It is not affiliated with, sponsored or endorsed by any of the websites it is pointed at. Each site belongs to its owner. You choose which pages it reads.

# Actor input Schema

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

the websites to read, one URL per site; each site returns one row of the contacts it publishes, or a status row saying why it could not

## `maxRequestsPerStartUrl` (type: `integer`):

the most pages fetched for one site, counting the start page and the contact pages it leads to (default 8)

## `mergeContacts` (type: `boolean`):

on: every page a site answers folds into one row per site; off: one row per fetched page

## `maxDepth` (type: `integer`):

how far to follow the site's own links looking for contact pages. 1 reads the start page plus pages it links to (default 1)

## `maxRequests` (type: `integer`):

a fetch cap shared across the whole run; leave unset for no cap

## `sameDomain` (type: `boolean`):

on (default): only follow links on the site's own host; off: contact links pointing elsewhere are followed too

## `considerChildFrames` (type: `boolean`):

also read same-host embedded frames: contacts living inside an iframe on the page

## `scrapeSocialMediaProfiles` (type: `object`):

per-network switches. A network set to false is not extracted; unset reads them all

## `proxyConfig` (type: `object`):

Apify proxy settings for the fetches. Residential proxies help on sites that refuse datacenter traffic

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

the most rows one run returns; the run stops there and says so

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.carbonvisuals.com/contact"
    },
    {
      "url": "https://www.cas.org/contact"
    },
    {
      "url": "https://www.acxiom.com/contact-us/"
    }
  ],
  "proxyConfig": {
    "useApifyProxy": true
  },
  "maxItems": 100
}
```

# Actor output Schema

## `rows` (type: `string`):

The run's dataset of contact rows. Each row carries one site's published emails, phones, social links, form URL and per-field provenance, or an uncharged miss verdict.

## `summary` (type: `string`):

The run's counts in the key-value store: rows fetched and returned, events charged, duplicates dropped, rows left over a charge limit.

# 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.carbonvisuals.com/contact"
        },
        {
            "url": "https://www.cas.org/contact"
        },
        {
            "url": "https://www.acxiom.com/contact-us/"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pradio/website-email-phone").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.carbonvisuals.com/contact" },
        { "url": "https://www.cas.org/contact" },
        { "url": "https://www.acxiom.com/contact-us/" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("pradio/website-email-phone").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.carbonvisuals.com/contact"
    },
    {
      "url": "https://www.cas.org/contact"
    },
    {
      "url": "https://www.acxiom.com/contact-us/"
    }
  ]
}' |
apify call pradio/website-email-phone --silent --output-dataset

```

## MCP server setup

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

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/wktmgsvUjE6wCxixs/builds/57yjbjvNQzafcqx3J/openapi.json
