# Website Contact & Socials Extractor (`entityplane/website-contact-socials-extractor`) Actor

Extract emails, phone numbers and social media links from company websites. Flat price per website, with browser rendering and proxies included.

- **URL**: https://apify.com/entityplane/website-contact-socials-extractor.md
- **Developed by:** [EntityPlane](https://apify.com/entityplane) (community)
- **Stats:** 2 total users, 1 monthly users, 50.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 domain with contacts

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

## Get emails, phone numbers and social media links from websites

Extract contacts from company websites without having to manage crawling infrastructure yourself. Browser rendering, proxies, bandwidth and crawler compute are included in the result flat price per website.

The crawling workload runs on our own EntityPlane infrastructure rather than on Apify, so websites that require more expensive processing do not create separate Apify compute or proxy charges.

The crawler prioritizes pages where businesses are most likely to publish contact information, such as contact, about and team pages. It handles proxies and retries automatically, and when a website requires JavaScript rendering, it launches a browser to find contacts that are not present in the initial HTML.

The crawler visits a limited number of pages per website. It does not crawl an entire directory, marketplace or aggregator to collect every contact listed there. For example, submitting Clutch will not return contacts for all companies listed on Clutch. Submit the individual companies' websites instead.

Result pricing is per website, not per crawled page. This keeps crawling costs predictable even when a site requires browser rendering, proxies or more processing.

In addition to web pages, the crawler reads PDF documents and vCard (`.vcf`) contact cards found on the website, such as brochures and downloadable contact cards. Scanned PDFs without a text layer are not read.

Besides **emails** and **phone numbers**, the crawler extracts links to **Facebook, Instagram, LinkedIn, X/Twitter, TikTok, YouTube, WhatsApp, Telegram, Threads, Snapchat, Pinterest, Reddit, Discord, Twitch, GitHub, CodePen, Google Maps, Yelp and Tripadvisor**. It does not scrape or analyze the content of those external profiles. It only returns links found on the submitted website, with duplicates removed.

### How it works?

You provide the websites. The Actor submits your input to our backend, which crawls the target websites and extracts contacts using our infrastructure and proxies. The Actor handles scheduling, billing and streaming results into your Apify dataset with a fixed memory allocation.

1. Add your websites in the Input tab, one per line, as full URLs or bare domains such as `example.com`.
2. Start the run. Results appear in your dataset as each domain finishes.
3. Open the results in the Output tab. Each row shows what happened to the domain (`outcome`), the page and time where each contact was found (`sources`) and when the crawl completed (`crawled_at`).

You can download the dataset in formats including JSON, HTML, CSV and Excel. Use Apify's API, scheduling and integrations to automate runs and send results to other tools.

### How much does it cost?

**Run cost = Actor start fee + input processing fees + result fees.** See this Actor's **Pricing tab** for current rates.

**All-inclusive results:** the result fee covers every page crawled, browser rendering, proxies, bandwidth and compute. There are no per-page, proxy or browser surcharges.

The input processing fee, listed in the Pricing tab as `input-url-processed`, is a flat fee per submitted URL entry, including repeated URLs, invalid entries and rejected domains. A separate result fee applies per website for each fresh or cached result.

| Event                                    | When it's charged                                                                                 |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Actor start                              | Once per run, including runs with invalid input or an exhausted allowance.                        |
| Input processing (`input-url-processed`) | Once per submitted entry, including duplicates, invalid entries and rejected URLs.                |
| Domain with contacts                     | A freshly processed domain where at least one contact, including a social media link, was found.  |
| Domain without contacts                  | A freshly processed domain where the crawl completed but no contacts were found.                  |
| Cached domain                            | An eligible result already available in our shared cache. Cached results use a lower result rate. |

Each domain can incur at most one result fee. The only exception is an apex domain and its `www` version, which are treated as the same website. For example, `example.com` and `www.example.com` share a single result fee. If you submit both, each entry still incurs the input processing fee, but the pair can incur only one result fee. The other row will have `charged_event: null`.

Blocked, unreachable, redirected and rejected domains have no result fee. The input processing fee still applies. The run-level Actor start fee also applies.

Automatically discovered pages and redirects add no input processing fees.

#### Cached results

We maintain a shared cache of previously processed public website data. If an eligible result already exists within your configured maximum result age, we return it without crawling the website again and charge the lower cached rate.

This reduces duplicate crawling and makes repeated or overlapping runs cheaper. You can control the maximum acceptable cache age in the Input tab.

#### Pricing example: 10,200 URLs across 10,000 domains

Suppose your `startUrls` list contains 10,200 URL entries covering 10,000 distinct domains. The extra 200 entries are repeated URLs or additional URLs on those same domains. The Actor processes the full list and returns 6,000 fresh results with contacts, 2,000 fresh results without contacts, 1,000 cached results and 1,000 blocked domains.

The charge consists of:

- 1 Actor start event.
- 10,200 input processing fees — one for each submitted entry in `startUrls`.
- 6,000 domain-with-contacts events.
- 2,000 domain-without-contacts events.
- 1,000 domain-cached events.
- 1,000 blocked domains — websites that blocked crawler access; no result fee.

Multiply each count by its rate in the Pricing tab to estimate the total.

This example shows the billing calculation, not an expected success rate.

#### Setting a charge limit

Each entry pays the input processing fee once, and each website pays at most one result fee. The highest result rate is defined by the billable event 'Domain with contacts', so a run can never cost more than:

**Actor start fee + number of entries × (input processing rate + domain-with-contacts rate)**

Set the maximum charge per run in the run options to at least this amount, and the charge limit will never stop your run early. Use the current rates from the Pricing tab. The actual cost is usually lower: cached results and websites without contacts use lower result rates, and blocked, unreachable, redirected and rejected websites pay no result fee.

### Free plan limits

Apify Free-plan users can process up to 10 websites per run and 50 websites per rolling 30 days.

Free-plan users also share a daily processing allowance. If a limit is reached, the Actor processes only the websites that fit within the remaining allowance and reports how many were skipped.

These limits are set by this Actor, not by Apify, and apply to users on Apify's Free plan. Upgrade to a paid Apify plan to process full lists.

### Input

Configure these fields in the Input tab:

| Field              | Description                                                                                                |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| `startUrls`        | Websites to process. Submit one address per website, as a full URL or a bare domain such as `example.com`. |
| `maxCacheAgeHours` | Maximum age of cached results to accept. Default 720 hours (30 days), minimum 4 hours.                     |

For API use or the JSON input editor:

```json
{
    "startUrls": ["https://example.com", "example.org"],
    "maxCacheAgeHours": 720
}
```

Automatic redirects between hosts are limited to the apex domain and its `www` version, such as `example.com` ↔ `www.example.com`. The crawler does not follow redirects to other websites or other subdomains, such as `shop.example.com`.

A run accepts up to 100,000 entries, including duplicates and invalid entries.

Invalid entries go into the separate **Rejected inputs** output.

### Output

The **Contacts** dataset contains one row per processed host. Rejected input entries are stored in a separate dataset. This example omits some contact columns and source entries:

```json
{
    "domain": "example.com",
    "outcome": "contacts_found",
    "start_urls": ["https://example.com", "https://example.com/about"],
    "redirected_from": null,
    "redirected_to": null,
    "emails": ["hello@example.com"],
    "phones": ["+14155550123"],
    "linkedin": ["https://www.linkedin.com/company/example"],
    "instagram": [],
    "counts": {
        "emails": 1,
        "phones": 1,
        "linkedin": 1,
        "start_urls": 2
    },
    "sources": {
        "emails": {
            "hello@example.com": {
                "url": "https://example.com/contact",
                "seen_at": "2026-01-15T12:00:25Z"
            }
        }
    },
    "crawled_at": "2026-01-15T12:00:45Z",
    "charged_event": "domain-with-contacts"
}
```

- `outcome` is one of `contacts_found`, `no_contacts_found`, `blocked`, `unreachable` or `redirected`.

- **Contact columns:** Every row includes all contact columns as arrays of up to 500 strings. Columns with no values contain `[]`. `counts` shows the full total for each column.

- `sources` points to the page and time where each contact was seen during the crawl.

- **Row-size limits:** In rare cases, a row is too large to store. We then trim values and their matching sources evenly across contact types, with the largest lists absorbing the trimming. `counts` keeps the full totals so you can identify truncated rows.

- `start_urls` lists the exact input URLs that resolved to this domain, letting you match results to your original input. Several URLs on the same host share one row. The array includes up to 50 URLs; `counts.start_urls` shows the full number.

- `redirected_to` lists the destination hosts for a redirected domain. When a redirect is followed automatically, the resulting row uses `redirected_from` to identify the original host. See the FAQ for redirects that need a new run.

- `crawled_at` records when the crawl behind the result completed. For cached results, it predates your run and falls within your maximum result age when selected. It is `null` for `redirected` rows, which have no crawl result of their own.

- `charged_event` identifies the result pricing event: `domain-with-contacts`, `domain-without-contacts` or `domain-cached`. It is `null` when no result fee applies. Input processing fees apply separately, once per submitted entry.

#### Contact columns

| Column        | Platform    | Values                          |
| ------------- | ----------- | ------------------------------- |
| `emails`      |             | Email addresses                 |
| `phones`      |             | Phone numbers                   |
| `linkedin`    | LinkedIn    | Profile and company page URLs   |
| `facebook`    | Facebook    | Profile and page URLs           |
| `instagram`   | Instagram   | Profile URLs                    |
| `twitter`     | X / Twitter | Profile URLs                    |
| `youtube`     | YouTube     | Channel URLs                    |
| `tiktok`      | TikTok      | Profile URLs                    |
| `whatsapp`    | WhatsApp    | Business and profile links      |
| `telegram`    | Telegram    | Channel, group and profile URLs |
| `threads`     | Threads     | Profile URLs                    |
| `pinterest`   | Pinterest   | Profile and board URLs          |
| `reddit`      | Reddit      | Community and profile URLs      |
| `snapchat`    | Snapchat    | Profile URLs                    |
| `discord`     | Discord     | Server invite URLs              |
| `twitch`      | Twitch      | Channel URLs                    |
| `github`      | GitHub      | Profile and organization URLs   |
| `codepen`     | CodePen     | Profile URLs                    |
| `google_maps` | Google Maps | Location URLs                   |
| `yelp`        | Yelp        | Business listing URLs           |
| `tripadvisor` | Tripadvisor | Business listing URLs           |

The **Contacts** view shows the flat columns for spreadsheets; the **Full** view adds details such as `sources`, `counts` and `redirected_from`.

#### Rejected inputs

Open **Rejected inputs** in the run's Output tab to see refused inputs and domains, including domains we excluded at the owner's request or for legal or policy reasons. Each row identifies what was rejected and why:

```json
{
    "input": "not a url",
    "rejection_reason": "not_a_url"
}
```

`input` identifies the refused entry or domain, and `rejection_reason` explains why. Invalid entries use `not_a_url`. `this domain is not allowed` marks a domain that was never crawled because the website owner asked us not to crawl it, or we excluded it for legal or policy reasons. Other reasons are more specific, for example `government domains are not supported` or `IP addresses are not accepted`. Rejected entries incur the input processing fee, with no result fee. The run-level Actor start fee also applies.

Values longer than 2,048 UTF-8 bytes are shortened to a preview in this dataset's `input` field. For these rows, `rejected_input` contains `truncated: true`, `original_bytes` and the full value's `sha256` fingerprint.

The dataset is available even when it is empty or every entry is rejected. If every entry is invalid, the Contacts dataset stays empty and no extraction task starts. An empty rejection report means no entries have been rejected so far; check the run status for failures or work that was not processed.

### Tracking progress

While a run is going, the key-value store record `PROGRESS` holds a JSON snapshot updated as domains finish:

```json
{
    "phase": "streaming",
    "scheduled": 400,
    "processed": 123,
    "withContacts": 80,
    "withoutContacts": 30,
    "cached": 10,
    "uncharged": 3,
    "rejected": 2,
    "unscheduled": null,
    "freePlan": null,
    "finalStatus": null,
    "updatedAt": "2026-08-28T18:05:12.000Z"
}
```

`rejected` counts entries in the separate **Rejected inputs** output, including invalid entries and domains we declined to crawl. `unscheduled` is `null` unless the run had to stop scheduling early; then it holds `{ "count": N, "reason": "charge_limit" }`. `count` is the number of `startUrls` entries that were never scheduled, counted like input processing fees: every entry, including repeated URLs and several URLs on one website. Those entries need a new run.

`scheduled` and `processed` count hosts, including those reached through automatic redirects or ending in a policy rejection. These counts can differ from the number of billable websites. `uncharged` counts processed hosts without a result fee.

`freePlan` is `null` for paying users. For users on Apify's Free plan it holds `{ "kept": N, "omitted": M, "limit": ..., "retryAt": ... }`: how many websites the run was allowed to schedule, how many this Actor left out, which limit applied (`per_run`, `user_window`, `shared_window`, or `null` when nothing was cut) and, as an ISO instant, when the limit that cut your list frees up. `retryAt` is `null` when the cut came from the per-run limit or when nothing was cut.

The **Progress** link in the run's Output tab opens this record. It is stored in the run's default key-value store under the key `PROGRESS`.

### Stopping and resuming

You can stop and resurrect a run, but a resurrected run may repeat some dataset rows that were written just before the interruption. Deduplicate results by `domain`, and rejected inputs by `input` or `rejected_input.sha256`. Repeated rows are not charged twice.

If a new version of this Actor is published while one of your runs is unfinished, resurrecting that run can fail because the new version cannot safely continue the old run's billing. The run message then tells you to start a new run with the same input.

Stopping a run prevents new work from being scheduled, but domains already being crawled may still finish. Resurrect the run to collect any available results.

If our crawler is at capacity, the run stops and its message tells you when to resurrect it. The resurrected run continues where it stopped, and nothing already charged is charged again.

If the run stops because it reached the maximum charge per run you set in the run options, or its timeout, raise that limit or the timeout before resurrecting it. If the charge limit was reached before any website was scheduled, the run is closed; start a new run with a higher limit instead. If some `startUrls` entries could not be scheduled at all, the final status message shows how many were left out. Process those in a new run.

### FAQ

**What do you mean by website?**
A website is all pages on one exact domain name, such as `example.com`. Pages on it, such as `example.com/about`, belong to the same website. `www.example.com` is treated as the same website as `example.com` for result fees. Subdomains such as `shop.example.com` are separate websites.

**What do you mean by contact?**
A contact is an email address, a phone number or a link to one of the social platforms listed in the Contact columns table, such as a LinkedIn company page or a Google Maps location. A website where only a social media link was found still counts as a website with contacts.

**Do you guarantee to crawl every page of a site?** No. Each domain gets a limited number of pages. The crawler visits pages where businesses are most likely to publish contact information first, such as contact, about, team and imprint pages, and recognizes such pages in 9 languages. Sites that are expensive to crawl, such as those that need browser rendering, get fewer pages.

**Why does a single website take a minute or more?** The crawler visits several pages on each website and waits for each one to respond. Some websites respond slowly or only show their content after it is rendered in a browser, which adds time. Websites are processed in parallel, so a long list does not take a minute per website.

**Can I extract a whole directory such as Clutch?** No. This Actor is designed to help researchers find contact information published on actual company websites. It is optimized for crawling first-party business websites, not large directories, marketplaces or aggregators.

**Can I submit company names instead of websites?** No. Each entry must be a website address, such as `example.com` or `https://example.com`. A company name is rejected as `not_a_url` and still incurs the input processing fee.

**Why does the run show more domains than I submitted?** When a website redirects between its apex domain and its `www` version, the crawler follows the redirect and adds the other version as its own domain. It gets its own row, marked `redirected_from`, and the progress count grows by one. The added domain has no input processing fee, and the pair pays at most one result fee.

**Why did a domain come back `redirected`?** The site sent the crawler to another host and no contacts were found on the host you submitted. If contacts were found before the redirect, the row is `contacts_found` instead. Only redirects between the apex domain and its `www` version are followed automatically, with the resulting row marked `redirected_from`. Redirects to other websites or other subdomains are not followed. Submit the destination from `redirected_to` in a new run if you want its contacts.

**Why was a domain rejected?** Check its `rejection_reason` in **Rejected inputs**. `this domain is not allowed` means the website owner asked not to be crawled, or we excluded the domain for legal or policy reasons. Reasons such as `government domains are not supported` or `social media platforms are not supported` come from our rules on which kinds of sites we crawl. Rejected domains are kept out of Contacts.

**Why fewer rows than URLs?** Several URLs on the same host are one domain, so they produce one row. Free-plan limits, your run budget or an interrupted run can also reduce the number of websites processed. Check the run message for details.

**What does `crawled_at` mean on a cached row?** It is when that domain was last crawled. Cached rows are billed at the cached rate. The timestamp falls within your configured maximum result age when the result is selected.

**Are the contacts verified?** No. Values are validated for format only. We don't resolve social media URLs, check that an email is deliverable or a phone number is live.

### Support

Open the Issues tab to report a problem or ask a question. Include the run ID, the affected website and what you expected to see. Do not include API tokens or credentials.

### Legal

Use the extracted data in compliance with the laws that apply to you, including GDPR, CAN-SPAM and others, and with the terms of the websites you process. This Actor collects publicly available contact details and does not verify their reachability.

EntityPlane automatically extracts contact information and other publicly available data from webpages. It does not attempt to identify individuals or create individual profiles.

EntityPlane uses a shared data cache. Data extracted during an Actor run is stored in the cache and may be used to fulfill subsequent requests from other users. This allows us to provide faster results and reduce the cost of repeated data collection.

We try to be gentle on websites: the crawler limits how many requests it sends to a website at once. If you don't want your website crawled by us, or you found your contact details in our database and want them removed, email privacy@entityplane.com.

# Changelog

This Actor's version history is a separate document: https://apify.com/entityplane/website-contact-socials-extractor/changelog.md

# Actor input Schema

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

Websites to extract contacts from: full URLs or bare domains such as example.com, one per line. Submit one address per website. The processing fee applies per input entry, including duplicates and invalid entries. Up to 100 000 entries per run.

## `maxCacheAgeHours` (type: `integer`):

Domains we processed within this window are served from our cache at the cached pricing instead of being crawled again. Lower it when you need fresher data; the default is 30 days.

## Actor input object example

```json
{
  "startUrls": [
    "https://rawgister.com/"
  ],
  "maxCacheAgeHours": 720
}
```

# Actor output Schema

## `contacts` (type: `string`):

No description

## `rejectedInputs` (type: `string`):

Rejected inputs and domains, each with a reason. Always available; empty when no entries have been rejected.

## `progress` (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 = {
    "startUrls": [
        "https://rawgister.com/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("entityplane/website-contact-socials-extractor").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": ["https://rawgister.com/"] }

# Run the Actor and wait for it to finish
run = client.actor("entityplane/website-contact-socials-extractor").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": [
    "https://rawgister.com/"
  ]
}' |
apify call entityplane/website-contact-socials-extractor --silent --output-dataset

```

## MCP server setup

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

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/0upbAlRdfGcknhwjW/builds/lXMpp8F6Mc3pVCPQV/openapi.json
