Website Contact Scraper: Email, Phone, Social
Pricing
from $0.84 / 1,000 website rows
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 hours ago
Last modified
Categories
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 only | Home page + contact pages | |
|---|---|---|
| Sites where an email was found | 31 | 57 |
| Email addresses found | 36 | 154 |
| Unique addresses | 35 | 134 |
| Sites with a phone number | 34 | 41 |
| Phone numbers found | 67 | 225 |
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;textandmarkup- the address written in the page, including inside attributes and inline data;jsonld-schema.orgmarkup, 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) comand 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-fallbackorresolver-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, pluslatitudeandlongitude- 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_formandcontact_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, andscraped_atso 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-onlyDeliverableEmailis on whilecheckMxis 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:
- 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.
- 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.
- 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.
- No social profile enrichment. Profile links are returned; follower counts and profile details from Instagram, TikTok or YouTube are not.
- Mail check stops at the domain. MX or A record present or absent. No mailbox-level verification, and no claim of one.
- Crawl ceiling of 40 pages per site. Deep, unstructured sites that hide contacts below that are out of reach.
- Phone country is derived, not guessed. With no
+, no structured address and no national domain,e164staysnulland the raw string is still returned. schema.orgdata 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.