Website Contact & Socials Extractor
Pricing
from $15.00 / 1,000 domain with contacts
Website Contact & Socials Extractor
Extract emails, phone numbers and social media links from company websites. Flat price per website, with browser rendering and proxies included.
Pricing
from $15.00 / 1,000 domain with contacts
Rating
0.0
(0)
Developer
EntityPlane
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
15 hours ago
Last modified
Categories
Share
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.
- Add your websites in the Input tab, one per line, as full URLs or bare domains such as
example.com. - Start the run. Results appear in your dataset as each domain finishes.
- 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:
{"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:
{"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"}
-
outcomeis one ofcontacts_found,no_contacts_found,blocked,unreachableorredirected. -
Contact columns: Every row includes all contact columns as arrays of up to 500 strings. Columns with no values contain
[].countsshows the full total for each column. -
sourcespoints 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.
countskeeps the full totals so you can identify truncated rows. -
start_urlslists 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_urlsshows the full number. -
redirected_tolists the destination hosts for a redirected domain. When a redirect is followed automatically, the resulting row usesredirected_fromto identify the original host. See the FAQ for redirects that need a new run. -
crawled_atrecords when the crawl behind the result completed. For cached results, it predates your run and falls within your maximum result age when selected. It isnullforredirectedrows, which have no crawl result of their own. -
charged_eventidentifies the result pricing event:domain-with-contacts,domain-without-contactsordomain-cached. It isnullwhen 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 | Profile and company page URLs | |
facebook | Profile and page URLs | |
instagram | Profile URLs | |
twitter | X / Twitter | Profile URLs |
youtube | YouTube | Channel URLs |
tiktok | TikTok | Profile URLs |
whatsapp | Business and profile links | |
telegram | Telegram | Channel, group and profile URLs |
threads | Threads | Profile URLs |
pinterest | Profile and board URLs | |
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:
{"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:
{"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.