Website Contact Details Scraper – Emails, Phones & Socials avatar

Website Contact Details Scraper – Emails, Phones & Socials

Pricing

from $7.50 / 1,000 website scrapeds

Go to Apify Store
Website Contact Details Scraper – Emails, Phones & Socials

Website Contact Details Scraper – Emails, Phones & Socials

Give it a list of websites or domains: it crawls each site's contact, about, team and impressum pages and returns one clean lead record per website with emails, phone numbers (E.164) and LinkedIn, Facebook, Instagram, X, YouTube, TikTok, WhatsApp and other social profiles.

Pricing

from $7.50 / 1,000 website scrapeds

Rating

0.0

(0)

Developer

Zee Lex

Zee Lex

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Share

Give it a list of websites or domains. For each one it crawls the most useful pages — homepage, contact, about, team, impressum/imprint and footer links — and returns one merged, deduplicated lead record per website with:

  • Emails, including addresses hidden by Cloudflare email protection or written as name [at] domain [dot] com. Junk such as logo@2x.png, you@example.com and tracking keys is dropped.
  • Phone numbers, normalized to E.164 (+14155550132), plus the raw text as found. Dates, prices, postcodes and VAT/registration numbers are not mistaken for phones.
  • Social profiles: LinkedIn, Facebook, Instagram, X/Twitter, YouTube, TikTok, Pinterest, GitHub, WhatsApp, Telegram and Threads. Share buttons, posts and platform home pages are left out.
  • The homepage title, meta description and language, and the URLs of the site's contact pages.

It works over plain HTTP, so it is fast and cheap. An optional browser fallback handles sites built entirely with JavaScript.

Use cases

  • Lead generation: turn a list of company websites into emails, phone numbers and LinkedIn pages ready for outreach.
  • Enrich Google Maps or LinkedIn lists: add contact details to businesses you scraped elsewhere, using their website column.
  • CRM enrichment: fill missing emails, phones and social links for the accounts in your CRM, and spot dead websites (status: "unreachable").
  • Market research: see which competitors are active on which social networks.

Input example

{
"startUrls": [
{ "url": "https://www.example-dental.com" },
{ "url": "acme-widgets.co.uk" }
],
"websites": "beispiel-gmbh.de\nhttps://www.example.com.my/about",
"maxPagesPerSite": 10,
"maxDepth": 2,
"sameDomainOnly": true,
"prioritizeContactPages": true,
"includeEmails": true,
"includePhones": true,
"includeSocials": true,
"phoneCountryHint": "",
"outputPerPage": false,
"useBrowserFallback": false
}

The inputs:

  • Websites can be full URLs or bare domains. Inputs on the same registrable domain (www.example.com, shop.example.com, example.com/contact) count as one website.
  • maxPagesPerSite includes the homepage. Contact-type pages are fetched first. Ordinary links may use only half of what is left of the limit, so contact pages found deeper still fit.
  • phoneCountryHint lets the scraper read numbers written without a country code, such as "(415) 555-0132". When it is empty:
    • the website's country domain is used (.my → MY, .co.uk → GB, .de → DE);
    • on .com/.org sites, only +country numbers are read from text;
    • tel: links are always read.

An empty input ({}) scrapes apify.com and python.org with 5 pages each and finishes in well under a minute.

Output example

One record per website (type: "website"):

{
"type": "website",
"inputUrl": "acme-widgets.co.uk",
"domain": "acme-widgets.co.uk",
"siteUrl": "https://www.acme-widgets.co.uk",
"siteTitle": "Acme Widgets — Industrial Widgets since 1999",
"description": "Acme Widgets makes industrial widgets.",
"language": "en-GB",
"emails": ["sales@acme-widgets.co.uk", "support@acme-widgets.co.uk", "acme.widgets@gmail.com"],
"emailsSameDomain": ["sales@acme-widgets.co.uk", "support@acme-widgets.co.uk"],
"phones": ["+442079460018", "+441614960000"],
"phonesRaw": ["020 7946 0018", "+44 161 496 0000"],
"linkedin": ["https://www.linkedin.com/company/acme-widgets"],
"facebook": ["https://www.facebook.com/acmewidgets"],
"instagram": ["https://www.instagram.com/acmewidgets"],
"x": ["https://x.com/acmewidgets"],
"youtube": ["https://www.youtube.com/@acmewidgets"],
"tiktok": [],
"pinterest": [],
"github": [],
"whatsapp": ["https://wa.me/447700900123"],
"telegram": [],
"otherSocials": [],
"contactPageUrls": ["https://www.acme-widgets.co.uk/contact-us"],
"pagesCrawled": 8,
"status": "ok",
"error": null,
"scrapedAt": "2026-10-29T08:00:00.000Z"
}

All values above are examples.

status is one of:

  • ok: contact details were found.
  • no_contacts: the site was crawled, but nothing was found.
  • unreachable: DNS failure, connection refused or timeout. error says which.
  • blocked: 403/429 or a bot-challenge page, still blocked after one retry on a new session.

The other fields:

  • emailsSameDomain lists the emails on the website's own domain or its subdomains. Those come first in emails.
  • otherSocials holds Threads profiles.

With outputPerPage: true, every crawled page also gets a free record: { "type": "page", "domain", "url", "emails", "phones", "phonesRaw", "socials" }.

The run summary is in the key-value store under OUTPUT:

{ "sites": 250, "sitesWithEmail": 191, "sitesWithPhone": 168, "sitesWithSocials": 203, "unreachable": 9, "blocked": 4, "pagesCrawled": 1840 }

Pricing

Pay per event:

  • website is charged once per website record with status ok or no_contacts. Unreachable and blocked websites are free.
  • page-browser is charged per page rendered in the browser. This only happens with useBrowserFallback, and only for pages that had almost no text over HTTP.

Per-page records are free. The run stops cleanly when your maximum cost per run is reached.

Setup note for the Actor owner: in Monetization, create the events website and page-browser. Do not enable the default per-dataset-item event.

How it works

  1. Each website is normalized to its origin and crawled from its homepage, following redirects (http → https, apex → www). If https fails at the network level, plain http is tried once.
  2. Links on the same registrable domain are ranked. Contact, about, team, impressum, legal, privacy, support, staff, people and careers pages, in many languages, come first. Crawling stops at maxPagesPerSite and maxDepth. These are skipped:
    • PDFs, images, archives and other files
    • login and cart pages
    • pages over 5 MB
  3. Extraction runs on every page:
    • mailto: and tel: links
    • the visible text, with obfuscated addresses decoded
    • the raw HTML, with HTML entities decoded
    • Cloudflare-protected emails
    • JSON-LD structured data (email, telephone, sameAs)
  4. Merging. Everything a website's pages yielded is merged and deduplicated into one record.
  5. Browser fallback (optional). After the HTTP crawl, pages with almost no text are rendered in a headless browser and read again. Links that only appear after rendering are followed within the same limits.
  6. Safety:
    • One failing website never stops the others.
    • About 45 seconds before the run's timeout, the scraper stops and saves a record for every website with what it has collected.
    • The run fails only if every website was unreachable.

FAQ

Why are some phone numbers only in phonesRaw? Some numbers can't be turned into a full international number: a tel: link without a country code on a .com site, or a short service number. Set phoneCountryHint to read national-format numbers on generic domains.

Why is emailsSameDomain empty when emails isn't? The business uses an address on another domain (Gmail, a parent company, an agency). Those are still in emails.

Does it find emails that are only shown after clicking or in images? Emails in images or behind a click-to-reveal button can't be read. Cloudflare-protected emails, HTML entities and "[at]"/"[dot]" spellings are decoded.

A website shows blocked. What can I do? The site's bot protection refused the request twice. Run those websites again with RESIDENTIAL proxy, and with useBrowserFallback for JavaScript-heavy sites. Blocked websites are not charged.

Can I give it a list from a spreadsheet? Yes. Paste the column into Websites (plain list), one website per line, or pass startUrls through the API.

Is it legal? The scraper reads public pages only. Emails and phone numbers of people are personal data. Make sure your use, especially cold outreach, complies with the GDPR, CAN-SPAM, PDPA and the other laws that apply to you.

Development

npm install
npm test # unit tests + end-to-end runs against local fixture websites (E2E_BROWSER=0 skips the browser run)
npm run fixture-sites # serve the fixture websites on :4611–4616 to try the Actor by hand
npm start # run locally (input in storage/key_value_stores/default/INPUT.json)