Contact Details Scraper for Websites | $0.60/1K
Pricing
$0.60 / 1,000 results
Contact Details Scraper for Websites | $0.60/1K
Get one row per website with cleaned email addresses, E.164 phone numbers, social profile links, company names, and source pages. Process a list of domains or URLs and pay only for websites where contact details are found.
Pricing
$0.60 / 1,000 results
Rating
0.0
(0)
Developer
KazKN
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
43 minutes ago
Last modified
Categories
Security
Limited permissionsShare
Website Contact Details Scraper
This contact details scraper turns a list of website URLs or domains into one structured dataset row per website, with published email addresses, phone numbers, social profiles, company names, and source pages. Use it to prepare lead lists, enrich company research, find a business contact page, or collect public contact channels without checking every website manually.
The Actor reads the site's HTML directly, then follows useful same-site links such as contact, imprint, legal notice, about, and privacy pages. It supports LinkedIn, Facebook, Instagram, X/Twitter, YouTube, and TikTok links. Every email and phone number includes the URL where it was first found, so you can inspect the source before using the data.
It is designed for sales and marketing teams, researchers, agencies, recruiters, and developers who need contact information from a known list of websites. It is a website scraper, not an email guessing tool: it returns details published on the pages it reads and does not invent addresses for people or companies.
Real output example
The following rows were produced by a local run of the current code on November 8, 2026, using the default maximum of five pages per website. They are copied unchanged from that run's dataset, not mock data. Empty arrays and null values are included to show what the dataset returns when a particular detail is not found.
[{"domain": "apify.com","inputUrl": "https://apify.com","status": "ok","statusReason": "5 page(s) read","billed": true,"pagesCrawled": 5,"emails": ["support@apify.com","hello@apify.com","legal@apify.com","privacy@apify.com"],"emailTypes": {"support@apify.com": "generic","hello@apify.com": "generic","legal@apify.com": "generic","privacy@apify.com": "generic"},"emailSourcePages": {"support@apify.com": "https://apify.com/","hello@apify.com": "https://apify.com/contact","legal@apify.com": "https://docs.apify.com/legal","privacy@apify.com": "https://docs.apify.com/legal"},"phones": [],"phoneSourcePages": {},"linkedin": "https://www.linkedin.com/company/apify","facebook": null,"instagram": null,"twitter": "https://x.com/apify","youtube": null,"tiktok": "https://www.tiktok.com/@apifytech","contactPageUrl": "https://apify.com/contact","legalPageUrl": "https://docs.apify.com/legal","companyName": "Apify Technologies s.r.o.","scrapedAt": "2026-11-08T05:32:08Z"},{"domain": "abcplumbing.com","inputUrl": "https://abcplumbing.com","status": "ok","statusReason": "1 page(s) read","billed": true,"pagesCrawled": 1,"emails": ["john@abcplumbing.com","kevin@abcplumbing.com"],"emailTypes": {"john@abcplumbing.com": "personal","kevin@abcplumbing.com": "personal"},"emailSourcePages": {"john@abcplumbing.com": "https://abcplumbing.com/","kevin@abcplumbing.com": "https://abcplumbing.com/"},"phones": ["+17195425631"],"phoneSourcePages": {"+17195425631": "https://abcplumbing.com/"},"linkedin": null,"facebook": null,"instagram": null,"twitter": null,"youtube": null,"tiktok": null,"contactPageUrl": null,"legalPageUrl": null,"companyName": "ABC Plumbing Pueblo & Colorado Springs Colorado","scrapedAt": "2026-11-08T05:32:07Z"}]
A row with status: "ok" means that at least one email address, phone number, or supported social profile was found. It does not mean that every output field is populated. For example, a site with only social links can still be an ok result.
The main fields are:
domainandinputUrl: the normalized website domain and the value supplied in the input.status,statusReason, andbilled: the outcome, a plain-language explanation, and whether the row was billed as a result.pagesCrawled: the number of pages that loaded successfully for the website.emailsandphones: deduplicated email addresses and phone numbers normalized to E.164 format.emailTypes: whether each address looksgenericorpersonal, based on naming rules.emailSourcePagesandphoneSourcePages: a map from each value to the first page where it was found.linkedin,facebook,instagram,twitter,youtube, andtiktok: the selected profile URL for each supported network, ornull.contactPageUrl,legalPageUrl, andcompanyName: useful pages discovered during the crawl and the organization or site name when available.scrapedAt: the UTC extraction time in ISO 8601 format.
The run's key-value store also contains a RUN_SUMMARY record with website counts, billed results, statuses, error types, pages read, transfer volume, proxy charges, invalid inputs, and whether the run stopped because its cost limit was reached.
Why choose this contact details scraper?
Contact-focused crawling, not just home-page extraction
A business often puts its email address on a contact page and its company details in an imprint or legal notice. The Actor starts with the supplied page, ranks same-site links, and prioritizes contact and legal pages before about and privacy pages. If the starting page has no contact or legal link, it can also try conventional paths such as /contact; German-speaking and French domains get relevant legal-page guesses.
The default limit is five pages per website and can be adjusted from 1 to 20. You pay per website result, not per page read, so a result assembled from several pages still counts once (the optional proxy fallback described below is the only per-page charge).
Cleaner emails with useful classification
Email extraction covers mailto: links, visible text, Cloudflare-obfuscated addresses, and common masked forms such as name [at] company [dot] com. Cleaning rules remove placeholders, documentation examples, Sentry and Wixpress artifacts, image-like strings, and common sample names.
Each surviving address is labeled generic or personal. Role addresses such as info@, support@, sales@, and press@ are generic; addresses that look like a named person are personal. This classification is rule-based, not a statement about the actual owner. Two optional filters let you retain only generic addresses or only addresses on the website's own domain.
Normalized phone numbers and traceable sources
Phone numbers can come from tel: links, JSON-LD, labeled text, and valid-looking numbers in visible page text. The Actor uses phone-number parsing and validation rules, then returns numbers in E.164 format, such as +17195425631. This validates the number's format and numbering rules; it does not call the number or confirm that it is active. The source-page map lets you review context and identify numbers belonging to another office or partner.
Social profiles without embedded-post noise
The Actor recognizes profile links in HTML anchors and schema.org sameAs data. It excludes sharing links, reserved paths, embedded posts, and video URLs that do not identify a profile. When several profiles for one network appear, it prefers a profile whose handle resembles the domain or company name, then the most frequent one.
Transparent outcomes and result-based pricing
The price is $0.60 per 1,000 results, equivalent to $0.0006 per billed website. A result is one dataset row with status: "ok" and at least one extracted email, phone number, or supported social profile. Multiple contacts from the same website do not create additional result charges.
Rows with no_contacts, blocked, js_only, not_found, or error are delivered with billed: false and are free. You can inspect these rows instead of guessing why a website produced no contacts. Optional proxy-page events, when proxy fallback is available and enabled, are separate from the $0.60/1,000 result price.
You can also set a maximum cost per run on Apify. The Actor checks the remaining result budget before processing another website and stops cleanly when the limit is reached.
Lightweight runs with honest limitations
The fetcher uses direct HTTP with a Chrome-like TLS/HTTP2 fingerprint and does not start a browser. In a recorded 40-website run on an earlier beta build (0.1.1), it processed all 40 sites in 54.169 seconds and delivered 31 billable rows. A separate battery check processed four websites with four contact rows in 3.815 seconds. These are recorded examples, not a speed or yield guarantee; response times, blocking, page limits, and website content affect every run.
No browser also means no JavaScript rendering. Sites that publish useful contact details only after JavaScript execution can return js_only. Sites behind bot challenges or access controls can return blocked. These outcomes are not billed as results. The Actor does not promise to bypass every protection system or extract every address on every website.
FAQ
What counts as one result?
One result is one website row with status: "ok". The row must contain at least one email address, phone number, or supported social profile. Reading several pages or finding several contacts on that website still produces one result. The result price is $0.60 per 1,000 results.
Do I pay for websites with no contact details?
No. no_contacts, blocked, js_only, not_found, and error rows are not billed as results. They remain in the dataset with billed: false and a statusReason to help you audit the run. If you enable an available proxy fallback, separate proxy-page charges apply only to successfully fetched proxy pages associated with websites that finish as ok.
What input can I provide?
Use a list of full URLs such as https://example.com/contact or bare domains such as example.com. URLs without a scheme are treated as HTTPS. The Actor creates at most one row per normalized domain and skips duplicate domains, so example.com and www.example.com do not become separate websites. Invalid entries are skipped and recorded in the run summary.
Will it find every email address or phone number on a website?
No. It reads a bounded selection of pages, stays on the supplied website and relevant same-site subdomains, and extracts supported formats from HTML. Details on unvisited pages, inside images or documents, behind a login, or rendered only by JavaScript may be missed. Increasing maxPagesPerDomain can widen the search but cannot remove those limits.
Does the Actor guess or verify email addresses?
It does not generate likely addresses from employee names, and it does not perform mailbox or deliverability checks. It extracts addresses found in the pages it reads. Cleaned and normalized does not mean that an address is active, monitored, or suitable for a particular campaign.
What is the difference between generic and personal emails?
Generic addresses look like role or organizational mailboxes, such as info@, contact@, billing@, or a brand name. Personal addresses look like they belong to a named person. The classification uses local-part and domain rules, so edge cases can be mislabeled. Enable onlyGenericEmails if you want to discard addresses classified as personal.
Why does a row contain an email from another domain?
A page may mention an agency, reseller, partner, service provider, or another office. These addresses are retained by default and linked to their source page. Turn on sameDomainEmailsOnly to keep only addresses whose registrable domain matches the website. Phone numbers from other offices or partners can also appear; use phoneSourcePages to review them.
Does it work with JavaScript-only websites?
The Actor does not execute JavaScript. If the HTML contains almost no visible text and relies on scripts to render the site, the website can be labeled js_only. Some JavaScript-heavy sites still publish useful links or structured data in their HTML and can produce results, but full browser rendering is not provided.
Can it scrape sites protected by Cloudflare, CAPTCHA, or other bot controls?
Direct requests may be rejected or shown a challenge. The Actor recognizes common blocking responses and returns blocked instead of treating a challenge page as contact data. Optional proxy fallback retries a blocked page through the configured proxy when that feature is available. It can help on some sites, but recovery is not guaranteed, and separate proxy-page pricing may apply.
Why did my run finish as failed when the dataset contains rows?
A run fails if it delivers no ok result. It also fails when more than half of the processed websites are blocked, even if contact rows were found for the others. Those successful rows remain available in the dataset. This behavior prevents a mostly blocked or empty extraction from being reported as a successful run.
Can I export the data as JSON, CSV, or Excel?
Yes. Results are stored in the run's default Apify dataset, where you can use the standard dataset export formats or retrieve items through the API. Nested fields such as email types and source-page maps are most naturally represented in JSON; flattened tabular exports are useful when you mainly need domains, emails, phones, and profile links.
Where can I see errors and run totals?
Open the RUN_SUMMARY record in the run's default key-value store. It includes processed and billed counts, a breakdown by status, errors grouped by type, pages read, and invalid input entries. Individual rows also contain status and statusReason for website-level troubleshooting.
Is the output compliant with GDPR or other privacy rules?
The Actor does not certify legal compliance. Publicly available information can still be personal data, especially addresses classified as personal. You are responsible for having a valid basis for collection and use, honoring website terms and applicable privacy or marketing laws, and handling deletion or objection requests where required. Consider onlyGenericEmails when named-person addresses are unnecessary.
Does the Actor scrape social media platforms themselves?
No. It extracts links to social profiles from the websites it visits; it does not open LinkedIn, Facebook, Instagram, X/Twitter, YouTube, or TikTok to collect their internal data. Social links that exist only in embedded posts, videos, or unsupported JSON may not be detected.
Settings and API
Input settings
| Setting | Default | What it controls |
|---|---|---|
urls | Required | Websites to scan. Accepts full URLs or bare domains; duplicate domains are skipped. |
maxItems | 1000 | Maximum number of unique websites to process, from 1 to 1,000,000. |
maxPagesPerDomain | 5 | Maximum pages read per website, from 1 to 20. Contact and legal pages are prioritized. |
onlyGenericEmails | false | If true, removes addresses classified as personal. |
sameDomainEmailsOnly | false | If true, removes email addresses on other registrable domains. |
maxConcurrency | 20 | Number of websites processed in parallel, from 1 to 50. |
proxyConfiguration | Disabled | Optional proxy fallback for blocked pages. Direct access is always tried first. If the Actor's pricing has no proxy-page event, this setting is ignored and a warning is logged. |
Example input:
{"urls": ["https://apify.com","https://buffer.com","abcplumbing.com"],"maxItems": 1000,"maxPagesPerDomain": 5,"onlyGenericEmails": false,"sameDomainEmailsOnly": false,"maxConcurrency": 20,"proxyConfiguration": {"useApifyProxy": false}}
For API compatibility, startUrls is also accepted as a list of { "url": "https://example.com" } objects, but urls is the recommended input shown in the Console.
Python API with a list of URLs
Install the Apify API client with pip install apify-client, create an Apify API token, and make it available as the APIFY_TOKEN environment variable. The following code starts the Actor, waits for completion, and reads every dataset row:
import jsonimport osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("kazkn/website-contact-details-scraper").call(run_input={"urls": ["https://apify.com","https://buffer.com","https://abcplumbing.com",],"maxItems": 1000,"maxPagesPerDomain": 5,"onlyGenericEmails": False,"sameDomainEmailsOnly": False,"maxConcurrency": 20,"proxyConfiguration": {"useApifyProxy": False},})items = client.dataset(run["defaultDatasetId"]).list_items().itemsprint(json.dumps(items, indent=2, ensure_ascii=False))
Use the run ID returned by the client to inspect the run in Apify. Dataset items contain the contact rows shown above, while the default key-value store contains RUN_SUMMARY. For scheduled or repeated workflows, keep a fresh input list for each run and use the source URLs and scrapedAt timestamps to assess when each contact was collected.