Website Contact Scraper: Email, Phone, Social avatar

Website Contact Scraper: Email, Phone, Social

Pricing

from $0.84 / 1,000 website rows

Go to Apify Store
Website Contact Scraper: Email, Phone, Social

Website Contact Scraper: Email, Phone, Social

One row per website, not per page: emails, phone numbers in E.164, 31 social platforms, postal address, opening hours and VAT id. The mail domain is MX-checked, Cloudflare-obfuscated addresses are decoded, and no browser or proxy is involved.

Pricing

from $0.84 / 1,000 website rows

Rating

0.0

(0)

Developer

Snow Leo Data

Snow Leo Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 hours ago

Last modified

Share

Website Contact Scraper: Email, Phone and Social Profile Extractor

Give this Actor a list of websites and get back one clean row per website: every email address on the site, every phone number in international + form, social profiles across 31 platforms, the postal address, the opening hours and the company identifiers - and, for each address, whether its domain can receive mail at all.

It reads the sites with plain HTTP requests. No browser, no proxy, no API key. That is not a limitation dressed up as a feature: it is the reason one website costs one row instead of twenty billed page loads.

The number that matters most: half the contacts are not on the home page

Measured on 135 live business websites - shops, offices, workshops and restaurants in Berlin, Bristol and Austin, addresses taken from OpenStreetMap, list stored in tools/sites_sample.json so the measurement can be repeated command-for-command with python3 tools/measure.py:

Home page onlyHome page + contact pages
Sites where an email was found3157
Email addresses found36154
Unique addresses35134
Sites with a phone number3441
Phone numbers found67225

Reading one page finds an email on 31 of 135 sites. Reading the contact and imprint pages as well finds one on 57 - and returns 4.28 times as many addresses. A scraper that stops at the home page loses that silently: the run is green, the row is just empty.

The opposite extreme - crawling the whole site - is what you pay for elsewhere. This Actor ranks the links instead: impressum and imprint weigh most, contact and kontakt next, then about, team, support, locations, privacy. The 135-site run fetched 530 pages for 99 reachable sites, and every one of them is inside the single row you are billed for.

Addresses a plain text scrape cannot see

Four separate channels feed the emails column, and the row records which one found each address in emails_detail[].method:

  • mailto - the most reliable, and present on far fewer sites than people expect;
  • text and markup - the address written in the page, including inside attributes and inline data;
  • jsonld - schema.org markup, where a company often publishes an address that appears nowhere in the visible text;
  • cloudflare - Cloudflare's email protection, which replaces the address in the HTML with a hex string and rebuilds it with JavaScript in the browser. A scraper without a browser sees nothing there. This one decodes it: 25 addresses hidden this way on 3 of the 135 sites;
  • obfuscated - info (at) example (dot) com and its variants, matched on word boundaries so that "Attractive package at good rates" is never mistaken for an address.

Junk that looks like an address is removed before you are charged for it: image names such as logo@2x.png, tracking keys on sentry.io, theme placeholders such as info@example.com and 23 other placeholder domains, hex blobs and UUIDs.

One row per website, not one per page

The largest Actor in this niche bills per scraped page and prefills 20 pages per website. Its own README gives the rate: "$5 of prepaid credits ... around 2,500 pages", which is $2.00 per 1,000 pages, so its own default setting works out at $40.00 per 1,000 websites.

Here a website is one row. Whatever it took to read it - the home page, the contact page, the imprint, the sitemap lookup - is inside that row. The median price in this niche, measured across 199 priced Actors carrying "contact", "email" or "phone" in slug or title, is $4.00 per 1,000 results.

Does that address even work?

Every email domain is checked against DNS over HTTPS, once per run per domain, and the result is attached to each address:

  • mx_found - the domain has an MX record, or an A record it can fall back to under RFC 5321;
  • mx_hosts - the mail servers themselves;
  • mail_provider - Google Workspace, Microsoft 365, IONOS, Zoho, Proton, Namecheap and 15 more, read from those hosts;
  • mx_note - nxdomain, no-mx-no-a, a-record-fallback or resolver-unreachable.

What this proves and what it does not. A missing MX and a missing A record proves the domain cannot accept mail, so that address is dead. A present MX does not prove the mailbox exists. This Actor never says it does, and there is no field here claiming a verified mailbox.

Phone numbers you can actually dial

Numbers arrive from tel: links, from schema.org, from WhatsApp links and from the page text, and each one is normalised to E.164 - +442079460958, the one shape a CRM or an auto-dialer will accept.

The country is derived, never guessed: from the + already in the number, then from addressCountry in the site's structured data, then from a national domain (.de, .co.uk; .io and .co are deliberately not treated as countries). With no country available, the number stays in phones_raw and e164 is null - a wrong number costs more than an empty field. Set defaultCountry to fill those in when you know the list is from one country.

Two repairs come from live pages: a postcode glued to a number (78702 512.912.1612) and a house number trailing one ((512) 215 9778 1807) are trimmed group by group until a valid number appears, and never by adding or reordering digits. And the same number written three ways on three pages becomes one contact, deduplicated on the E.164 form - so you are not billed three times for it.

What else comes back in the row

The dataset schema declares 87 columns. Besides contacts:

  • Company from schema.org: company_name, legal_name, schema_type, company_description, logo_url, slogan, vat_id, tax_id, lei_code, duns, price_range, founding_date. Structured company data was present on 37 of the 135 sites.
  • Postal address split into fields: street, city, region, postal_code, country, country_code, plus latitude and longitude - on 22 sites in the sample.
  • opening_hours, one line per day (Mo 09:00-17:30) - on 12 sites.
  • Site facts useful for qualifying a lead: platform (15 different site builders and shop systems appeared in the sample, from WordPress and Shopify to Wix, Webflow, Duda and GoDaddy), generator, trackers, page_title, meta_description, language.
  • has_contact_form and contact_form_url - 23 sites carry a contact form, which is where you write when there is no address at all.
  • contact_page_url, imprint_page_url, pages - so you can see exactly where each fact came from.
  • The outcome of the request itself: reachable, http_status, blocked, error, redirected_off_domain, insecure_tls, and scraped_at so a row read months later still says when it was true.

Social profiles land both as plain columns (linkedin, twitter, facebook, instagram, youtube, tiktok, pinterest, telegram, whatsapp, discord, github, threads, snapchat, reddit, vk, xing, medium, vimeo, yelp, trustpilot) and in social_profiles covering 31 platforms. Share buttons, /intent/ links and platform home pages are not profiles and are dropped: 361 profiles across 82 sites in the sample are real ones.

A site that refuses still gets a row

By default, a site that answers 403 or no longer resolves comes back as a row saying so - reachable: false, blocked: true, http_status, error. Your list keeps its length and every address is accounted for. Switch on skipUnreachableSites to drop those rows instead and not pay for them.

Measured by running the 135-site sample through this very Actor, run UeFdbC59fuHm7AqgQ on Apify servers, with no proxy and no browser: 12 of 135 were refused and 100 opened, and 23 no longer resolve at all - stale entries in the source list rather than blocks. That run fetched 539 pages, found 261 email addresses, 508 phone numbers and 396 social profiles, checked 86 mail domains, and returned 121 rows after 14 duplicate domains were merged into the ones you already pay for.

Monitor mode: pay only for what changed

Set onlyChanged and the Actor remembers what each site returned - in a named key-value store, so the memory survives between runs, which the default store does not. On the next run only sites whose contacts changed are written, tagged NEW or UPDATED. Nothing changed means an empty dataset and no charge, and the log says so in plain words rather than leaving you guessing.

The fingerprint covers emails, phone numbers, social profiles, company name and reachability - not the page title or the tracker list, which change on every site deployment and would mark everything as changed. emitUnchanged returns the unchanged sites too, tagged UNCHANGED; monitorStoreName keeps several lists apart.

This is the shape of demand in this niche: the benchmark Actor has 218.7 runs per user, and the whole niche - 562 Actors, 257,019 users, 21,188,146 runs - averages 82.4 runs per user. People put lists on a schedule.

When the dataset is empty, the Actor says why

An empty dataset with a green run is the worst thing a paid Actor can do to you. Every empty result here carries a named reason in the REPORT record and a warning in the log. Ten of them:

  • no-start-urls - nothing was given to read;
  • all-urls-invalid - nothing in the list is a web address;
  • deliverable-without-mx-check - onlyDeliverableEmail is on while checkMx is off, so every row would be dropped whatever the sites contain;
  • email-kinds-exclude-everything - the kinds kept and the free-mail switch exclude every possible address;
  • charge-limit-reached - the run's charge limit is already used up;
  • all-sites-refused - every site refused or could not be reached;
  • no-contacts-found - the sites opened and carried no contact at all;
  • filtered-out - contacts were found and your filters removed every row;
  • nothing-changed - monitor mode, and nothing moved since last time;
  • nothing-collected - nothing at all, and no filter is to blame.

The first five are decided from the input alone, before a single byte is downloaded. A self-contradicting input is not something you should pay crawling time for: the run stops, names the reason and writes the report.

Filters, and the trap they usually hide

Twelve filters: onlyWithEmail, onlyWithPhone, onlyWithContacts, onlyWithSocialProfile, skipUnreachableSites, onlyDeliverableEmail, onlySameDomainEmail, skipFreeMailboxes, emailKinds, domainContains, textContains, excludeContains.

Two rules they all follow. An empty field is missing information, not a mismatch: a word filter never drops a site simply because that site published no structured data. And a filter never depends on a field this run did not collect - the one case where it could, onlyDeliverableEmail without checkMx, is caught before any request instead of quietly returning zero rows.

maxItems limits what is written and billed, and sites beyond the limit are never opened. With filters on, more sites are visited than the limit, because some rows will be dropped - but the delivered count still stops at your number.

Input at a glance

startUrls takes bare domains, full links, or link-in-bio pages. maxPagesPerSite (default 6, ceiling 40) and crawlStrategy (contact-pages, all-links, homepage-only) set how deep to go; useSitemap and guessContactPaths find contact pages that the home page never links to; sameDomain keeps the crawl on the site; scrapeIframes opens embedded frames; followLinkInBioPages reads the sites behind a bio page; checkMx and defaultCountry control data quality; concurrency and requestTimeoutSecs control speed; compactOutput drops empty fields and the detail arrays for AI agents and spreadsheets; onlyChanged, emitUnchanged and monitorStoreName run the monitor. 29 input fields in total.

Honest limitations

Written out rather than left for you to discover:

  1. No browser. Pages painted entirely by JavaScript give up less here than they would to a browser-based scraper. The trade is deliberate: it is what makes a whole website cost one row.
  2. No proxy. A site that blocks datacentre addresses stays blocked. Measured from Apify servers on the 135-site sample: 12 answered 401 or 403 and stay out of reach.
  3. No employee records. This Actor reads what the website publishes. It does not buy names, job titles or personal work emails from a B2B database, and there is no add-on here that does.
  4. No social profile enrichment. Profile links are returned; follower counts and profile details from Instagram, TikTok or YouTube are not.
  5. Mail check stops at the domain. MX or A record present or absent. No mailbox-level verification, and no claim of one.
  6. Crawl ceiling of 40 pages per site. Deep, unstructured sites that hide contacts below that are out of reach.
  7. Phone country is derived, not guessed. With no +, no structured address and no national domain, e164 stays null and the raw string is still returned.
  8. schema.org data depends on the site. 37 of 135 sites published it; the rest return the contact fields without the company block.

A line-by-line comparison with the strongest Actor in the niche - every input, every output field, every mode - is in COMPARISON.md.

FAQ

Can I paste a plain domain instead of a full URL? Yes. example.com, www.example.com and https://example.com/page all work, and the Actor tries www and non-www, plus plain HTTP, before calling a site unreachable.

What happens to a linktr.ee link? A link-in-bio page has no contacts of its own, so the Actor reads the sites it links to instead. Thirteen such hosts are recognised, including bio.link, beacons.ai, taplink.cc and carrd.co. Turn it off with followLinkInBioPages.

Why did a site come back with no email at all? Usually because the address is only in a contact form, or in an image, or the page is drawn by JavaScript. Check has_contact_form - if it is true, the company took messages through a form rather than publishing a mailbox. Raising maxPagesPerSite or switching crawlStrategy to all-links also helps on large sites.

Is a row with mx_found: true a verified email? No. It means the domain can receive mail. Mailbox-level verification is not part of this Actor, and no field here claims it.

How many pages will one site cost me? None, directly - you are billed per row, and a row is a website. For reference, the 135-site measurement fetched 530 pages in total.

Can I run this on a schedule without paying for the same data twice? That is exactly what onlyChanged is for. Unchanged sites are not written and not billed, and the run reports nothing-changed rather than looking broken.

Does it respect the sites it reads? It reads only public pages, at most 40 per site, with a per-host slow-down that doubles after any 429 and is shared by all threads. No logins, no forms submitted, no personal data bought from anywhere.

What is in the REPORT record? Sites requested, opened, blocked and unreachable; pages fetched; emails, phone numbers and social profiles found; duplicates dropped before billing; how many rows each filter removed; how many mail domains were checked; which hosts failed and why; and the named empty reason when the dataset is empty.

Which fields should I look at first for cold outreach? primary_email (a role mailbox on the site's own domain is preferred over a personal one on free mail), deliverable_emails, primary_phone, company_name, city and platform.

Is the data GDPR-safe? Everything returned is published by the site itself on a public page. Personal addresses are labelled personal in emails_detail[].kind so you can drop them with emailKinds if your legal basis only covers role mailboxes. What you do with the data is your responsibility.